FlawOpen

flawopen.com/XML 外部实体(XXE)/Python XML 解析器安全

Python 的 xml.etree.ElementTree 能防止 XXE 吗?

CWE-611:XML 外部实体引用参考页面
简要回答

对于经典的文件读取和网络 XXE,Python 内置的 Expat 解析器默认不会访问本地文件,也不会建立网络连接。但这不代表所有 XML 用法都安全:低于 2.7.2 的 Expat 可能遭受实体扩展和长 token 拒绝服务攻击。lxml 的默认值也因版本而异。请固定并显式配置解析器,或用 defusedxml 处理不可信 XML。

风险配置 — 版本和选项会改变暴露面

XXE(CWE-611):
解析器解析攻击者控制的 XML 中外部实体,可能读取本地文件或发起服务器端请求。
实体扩展 DoS:
嵌套或重复引用在展开时会过度消耗 CPU 或内存。
Expat:
Python 内置 XML 模块使用的 C 解析器;用 pyexpat.EXPAT_VERSION 检查实际运行版本。
DTD:
文档类型声明可定义实体,并可能根据解析器设置请求外部资源。
步骤 1

不可信 XML 到达解析器

上传文件、SVG、SAML 消息或集成数据越过信任边界并进入 XML API。

步骤 2

解析器实现和选项决定行为

标准库使用 Expat;lxml 使用 libxml2。版本、外部解析、实体扩展和选项决定可触发的行为。

步骤 3

解析或扩展造成影响

若启用外部解析,实体可请求本地或远程内容。易受攻击的扩展或长 token 也可能耗尽资源。

步骤 4

进程权限限制影响范围

文件权限与网络出口限制 XXE 影响;输入大小限制、更新解析器、安全选项和隔离可降低风险。

Python 当前安全指南指出,Expat 默认不访问本地文件或建立网络连接。但低于 2.7.2 的版本可能受 billion-laughs、quadratic-blowup、长 token 或过量内存使用影响。请检查 pyexpat.EXPAT_VERSION,因为 Python 可能使用内置或系统 Expat。这是依版本而异的资源耗尽风险,不代表当前 ElementTree 会解析外部实体。

加固配置 — 拒绝 DTD 与外部资源
# Standard-library external access is off by default,
# but old Expat builds have resource-exhaustion risks.
import xml.etree.ElementTree as ET
from xml.parsers import expat

print(expat.EXPAT_VERSION)  # inspect the runtime parser
root = ET.fromstring(untrusted_xml)

# lxml before 5.0 defaulted resolve_entities=True.
# Explicit unsafe settings illustrate the risky boundary.
from lxml import etree
parser = etree.XMLParser(resolve_entities=True, load_dtd=True)
root = etree.fromstring(untrusted_xml, parser)
区分三种 XML 解析器风险
from defusedxml.ElementTree import fromstring

# Reject DTD declarations when the format does not require them.
root = fromstring(untrusted_xml, forbid_dtd=True)

# If lxml is required, set its security options explicitly.
from lxml import etree
parser = etree.XMLParser(
    resolve_entities=False,
    no_network=True,
    load_dtd=False,
    huge_tree=False,
)
root = etree.fromstring(untrusted_xml, parser)
# Do not run XInclude on untrusted trees without a restricted loader.

为实际部署的解析器选择安全设置

defusedxml.ElementTree 默认禁止实体声明和外部引用;若输入格式不需要 DTD,也设置 forbid_dtd=True。使用 lxml 时显式设置 resolve_entities=False、load_dtd=False、no_network=True 和 huge_tree=False。当前版本默认只解析内部实体;lxml 5.0 之前默认会解析实体。不要对不可信树执行 XInclude,除非加载器受限。解析前限制输入及解压后大小;这些选项无法限制所有资源成本。

为什么要根据解析器版本审查默认值

“Python XML”并非单一配置。标准库依赖 Expat,解释器构建可能使用内置或系统库。lxml 包装 libxml2,并在 5.0 版更改实体解析默认值;包装器和显式选项也会改变行为。记录 Python、Expat、lxml、libxml2 版本,并在每个不可信输入点显式设置安全选项。

常见问题

ElementTree 默认会通过外部实体读取本地文件吗?

不会。Python 文档说明 Expat 默认不会访问本地文件或网络。检查 Expat 版本,并区分经典 XXE 文件访问与旧版本的资源耗尽问题。

当前 lxml 默认不安全吗?

不要跨版本一概而论。当前 XMLParser 默认只解析内部实体、不加载 DTD、不访问网络。lxml 5.0 将 resolve_entities 默认值从 True 改为 internal。请显式设置选项并检查包装器。

defusedxml 默认拒绝所有 DTD 吗?

不会。它默认禁止实体和外部引用,但 forbid_dtd 默认是 False。格式不需要 DTD 时请设为 True。

安全解析器能消除所有 XML 资源风险吗?

不能。限制压缩和解压后的输入大小,更新解析器,避免 huge_tree,也不要用不受限加载器处理不可信 XInclude。

主要参考资料

另请参阅 XML 外部实体注入(XXE,CWE-611):原理与防御方法 — flawopen.com .NET XmlReader 能防御 XXE 吗? Java XXE:DocumentBuilderFactory 默认安全吗?