XML 外部实体处理可能让解析器按照文档提供的引用,使用应用自身的权限获取资源。具体解析器及配置不同,结果可能是读取文件,也可能是向其他服务发起请求。支持 XML 不等于必然存在漏洞;需要先识别真正使用的库、已启用功能、资源解析器和后续处理步骤,才能判断输入究竟能够触发什么操作。
外部访问与内部展开不是一回事
外部解析实体可以用系统标识符指向资源。应用启用解析、进程又有能力访问时,读取文档便可能加载它。把内容反射到响应中只是泄露的一种形式;即使响应没有带回资源,向外发出的请求本身仍可能产生影响。访问范围同时受到解析策略、文件权限、网络控制和目标服务认证的限制,并不是一个 URL 就能保证拿到任意内部秘密。
内部实体则可以完全在文档中展开,不需要文件或网络。在保护不足的解析器中,重复嵌套替换可能消耗大量资源,这与 XXE 是不同问题。一个有明确上限的一千字符示例,只证明发生了替换,不能证明现代解析器允许无限放大。实体递归引用自身不符合 XML 规则,受支持的实现还可能在内存耗尽之前就拒绝过度放大,不能把所有解析器都描述成必然陷入死循环。
在解析开始前确定政策
输入格式不需要 DTD 时,应使用解析器支持的策略或回调拒绝文档类型声明。同时按具体实现关闭不需要的外部普通实体、参数实体和外部子集。库与版本的默认行为不同,仅禁止网络访问可能仍允许读取本地文件;只做 schema 校验,也可能已经晚于危险的初次解析。应检查实际入口怎样构建解析器,而不是在配置文件中找到一个相似名字就认为已经完成防护。
确实需要外部资源时,只把明确批准的标识映射到受控资源。schema 导入、XInclude 与转换器要单独审阅,XXE 开关未必控制这些加载器。还要限制字节数、嵌套深度与资源消耗,并维护解析库版本。一个短包装函数不可能自动抵御全部畸形输入、压缩炸弹或后续处理风险,政策边界应如实写明。
坏掉的样子
只在原始字节中搜索 ASCII 的 DOCTYPE 拼写,可能漏掉 UTF-16 之类编码后的声明;简单文本搜索又会误杀包含该字样的普通注释。应让理解编码和语法的解析器识别结构,再在声明边界执行政策。成功拒绝一条熟悉攻击字符串,不等于覆盖了入口接受的所有编码,也不等于找到了所有可能读取资源的路径。
SVG 上传可能进入服务器端 XML 解析器或图像转换器。该环节的配置决定是否加载外部引用,不能因为文件是 SVG,就声称每种浏览器都会自动解析外部实体。应检查预览、导入以及后续重解析的每一环;第一个解析器安全,不会替第二个开启不同加载器的组件提供保护。测试对象应当是实际管线,而不是只看文件扩展名。
快速验证
保存为 xxe_probe.py,使用受维护的 Python 3 执行 python3 xxe_probe.py。实验采用标准库 Expat 回调、固定内存字节和明确的小规模限制,不安装任何打开文件或发起网络请求的解析器。拒绝声明中的路径与域名只是测试输入,不会访问对应资源,也不会读取真实系统文件。
from xml.parsers import expat
MAX_BYTES = 4096
MAX_DEPTH = 32
def parse_without_dtd(data):
"""Bounded demonstration policy, not a general XML security library."""
if not isinstance(data, bytes):
raise TypeError("supply original encoded bytes")
if len(data) > MAX_BYTES:
raise ValueError("input too large")
parser = expat.ParserCreate()
text = []
depth = 0
def reject(*args):
raise ValueError("DTD and entity declarations are disabled")
def start(name, attrs):
nonlocal depth
depth += 1
if depth > MAX_DEPTH:
raise ValueError("too deeply nested")
def end(name):
nonlocal depth
depth -= 1
parser.StartDoctypeDeclHandler = reject
parser.EntityDeclHandler = reject
parser.ExternalEntityRefHandler = reject
assert parser.SetParamEntityParsing(expat.XML_PARAM_ENTITY_PARSING_NEVER)
parser.StartElementHandler = start
parser.EndElementHandler = end
parser.CharacterDataHandler = text.append
parser.Parse(data, True)
return "".join(text)
internal = ('<!DOCTYPE r [<!ENTITY a "0123456789">'
'<!ENTITY b "&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;">'
'<!ENTITY c "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">]><r>&c;</r>')
# A bounded baseline: ordinary internal expansion, not a resource-exhaustion test.
baseline = expat.ParserCreate()
chunks = []
baseline.CharacterDataHandler = chunks.append
baseline.Parse(internal.encode(), True)
assert len("".join(chunks)) == 1000
assert parse_without_dtd(b'<r>hello & goodbye</r>') == 'hello & goodbye'
assert parse_without_dtd(b'<r><!-- literal <!DOCTYPE is only a comment -->ok</r>') == 'ok'
external = '<!DOCTYPE r [<!ENTITY e SYSTEM "file:///not-read-by-this-demo">]><r>&e;</r>'
network = '<!DOCTYPE r SYSTEM "https://example.invalid/never-requested"><r/>'
svg = '<!DOCTYPE svg [<!ENTITY e "text">]><svg>&e;</svg>'
rejected = []
for name, payload in [
("internal", internal.encode()), ("file", external.encode()),
("external-subset", network.encode()), ("svg", svg.encode()),
("utf16-dtd", external.encode("utf-16")),
("oversize", b'<r>' + b'x' * MAX_BYTES + b'</r>'),
("depth", b'<r>' * (MAX_DEPTH + 1) + b'</r>' * (MAX_DEPTH + 1)),
]:
try:
parse_without_dtd(payload)
except ValueError:
rejected.append(name)
else:
raise AssertionError(f"policy accepted {name}")
assert len(rejected) == 7
try:
parse_without_dtd(b'<r>unfinished')
except expat.ExpatError:
pass
else:
raise AssertionError("malformed XML accepted")
print("Expat:", expat.EXPAT_VERSION)
print("PASS: ordinary text/comment accepted; seven policy cases and malformed XML rejected")
完整实验在 Python 3.12.14 通过,并打印链接的 Expat 版本。基线把有限内部实体链展开为恰好一千字符。限制解析器允许普通文字、预定义的与号实体以及无害注释;它拒绝内部声明、文件实体、外部子集、SVG DTD,也拒绝同一文件声明的 UTF-16 编码形式。这里证明的是解析政策生效,而不是敏感文件曾经被成功读出。
另外两个案例分别超过四千零九十六字节与三十二层嵌套限制;语法不完整的 XML 必须引发解析器语法异常。这些检查将主动政策拒绝与无关错误区分开来。UTF-16 对照尤其说明,限制发生在识别编码之后的结构边界,而不是依赖某一种 ASCII 字节模式。所有输入都很小,不需要真的制造内存耗尽来验证判断分支。
这是一项有边界的解析实验,不是对文件、网络目标或上传服务的利用测试。它不验证 schema 处理、转换器、后续重解析、压缩请求处理或生产资源隔离。处理不可信文档时,应使用受维护的加固 XML 设施,并让实际应用入口重新跑相应政策测试。示例通过不能替代整条输入链的审查,也不能推导所有下游组件采用了同一套配置。
延伸阅读
来源
- Python 3.12 Expat 接口,声明回调、参数实体策略与编码输入,已核验 2026-10-15。
- Python XML 安全,与库和版本相关的漏洞边界,已核验 2026-10-15。
- OWASP XXE 防护,按解析器限制外部资源,已核验 2026-10-15。
- W3C XML 1.0,实体、文档类型与禁止递归引用,已核验 2026-10-15。