必须用 defusedxml 替代 xml.etree.elementtree 解析微信 xml,因其默认禁用 dtd、外部实体和网络加载;若坚持用标准库,须显式构造 xmlparser 并设置 forbid_dtd=true 和 resolve_entities=false,且改用 parse() 而非 fromstring()。

直接用 xml.etree.ElementTree.fromstring() 解析微信推送或任何外部 XML 请求,等于主动打开 XXE 漏洞大门。必须禁用外部实体,且不能只靠“加个参数”应付了事。
为什么 xml.etree.ElementTree 默认不安全
Python 标准库的 xml.etree.ElementTree 在早期版本中默认解析 DTD 和外部实体(尽管 3.9+ 已默认禁用部分行为),但关键问题是:它不保证跨版本、跨平台行为一致;更严重的是,fromstring() 这类便捷函数**不接受 parser 参数**,你根本没法控制底层解析器行为。
- 攻击者发一个带
]>的请求,老版本 Python 可能直接返回服务器密码文件内容 - 即使新版本默认关闭,一旦代码里显式构造了
XMLParser()却漏掉resolve_entities=False,风险立刻回归 - 不用 DTD 也能触发 SSRF —— 某些 XML Schema 或 XInclude 功能在未禁用时同样危险
用 defusedxml 替代标准库是最省心方案
defusedxml 是专为防御 XXE 设计的 drop-in 替代库,所有接口与标准库一致,但默认禁用 DTD、外部实体、网络加载、递归展开等全部高危行为。
FastAPI + Flask 混合部署最佳实践,解决路由定义、API 代理等常见问题,适用于同时运行 FastAPI API 与 Flask 前端的场景。
- 安装:
pip install defusedxml - 替换导入:
from defusedxml.ElementTree import fromstring(不是xml.etree.ElementTree) - 原有代码几乎不用改:把
ET.fromstring(request.data)换成fromstring(request.data)即可 - 它还会自动限制嵌套深度和实体展开数量,防“Billion Laughs”类 DoS 攻击
如果坚持用标准库,必须显式传入安全 parser
xml.etree.ElementTree.fromstring() 本身不支持 parser 参数,所以必须改用 xml.etree.ElementTree.XMLParser() + xml.etree.ElementTree.parse() 组合,且 parser 配置缺一不可:
- 创建 parser 时必须同时设置:
XMLParser(resolve_entities=False, forbid_dtd=True, forbit_entities=True) - 不能只设
resolve_entities=False——forbid_dtd=True才真正禁止 DOCTYPE 声明,否则攻击者仍可绕过 - 解析时要用
parse(io.BytesIO(request.data), parser=parser),不能用fromstring() - 注意:
request.data是 bytes,别误传成 str,否则抛UnicodeDecodeError
Flask 路由里还要加 Content-Type 和结构校验
光靠解析器安全还不够。攻击者可能发 JSON 或表单数据伪装成 XML,或者构造畸形 XML 触发解析异常导致服务崩溃。
- 开头强制检查:
if not request.content_type.startswith("application/xml"):,拒绝非 XML 类型 - 捕获解析异常并返回 400,不要把原始错误堆栈暴露给客户端
- 简单校验根节点名是否符合预期(如微信推送必须是
xml),可用root.tag == "xml"快速过滤 - 对超大请求(如 >1MB)提前拦截,防止内存耗尽 —— 微信推送 XML 通常不超过 10KB
真正容易被忽略的点是:很多项目把 defusedxml 当成“可选加固”,结果上线后用标准库解析,只在测试环境换库。XXE 不需要复杂利用,一个带 SYSTEM 实体的 POST 就够了 —— 安全配置必须写死在生产代码里,不能靠部署时手动切换。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










