XML作为一种历史悠久的数据交换格式,至今仍广泛存在于接口对接、配置文件、SOAP服务、办公文档解析等场景中。很多开发者认为XML只是普通的数据载体,却忽略了XML解析器本身具备的强大能力:它支持外部实体引用、文件包含、网络请求等特性。一旦应用程序将未经校验的用户输入直接拼入XML文档,或者解析器默认开启了外部实体加载,攻击者就可以构造特殊的XML载荷,实现敏感信息读取、服务端请求伪造甚至远程代码执行。本文将从攻击原理、典型利用场景和防护方案三个层面,系统地讲清楚XML注入这类漏洞。

XML注入攻击的原理与类型
XML注入本质上分为两大类:第一类是狭义的XML注入,即攻击者通过在输入中插入XML标签或特殊字符,改变原有XML文档的结构,进而篡改业务逻辑;第二类是XXE(XML External Entity)注入,即利用XML解析器处理外部实体的能力,读取本地文件、发起网络请求或造成拒绝服务。
先看第一类。假设某系统生成订单的XML文档时直接拼接用户输入的备注字段:
<order> <user>zhangsan</user> <remark>--用户输入直接拼入这里--</remark> <price>199.00</price> </order>
如果用户在备注中输入</remark><price>0.01</price><remark>,最终生成的文档就变成了:
<order> <user>zhangsan</user> <remark></remark><price>0.01</price><remark></remark> <price>199.00</price> </order>
解析器读取到的第一个<price>值变成了0.01,订单金额被成功篡改。这就是最直接的XML注入危害:攻击者通过闭合标签、插入新节点的方式改变文档语义,实现越权操作或数据篡改。
第二类XXE则更加危险。XML规范定义了文档类型定义(DTD),允许在DTD中声明实体。攻击者可以声明一个指向本地文件的外部实体:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///C:\Windows\win.ini"> ]> <root>&xxe;</root>
当解析器处理&xxe;时,会按照SYSTEM标识符去读取C:\Windows\win.ini文件的内容并回显到解析结果中。如果目标系统使用Linux,攻击者只需把路径换成file:///etc/passwd。此外,把SYSTEM标识符改为http://内网地址/接口,XXE还能演变成SSRF,用于探测内网端口和服务,这也是云环境下XXE危害被放大的重要原因。
不同语言解析器的默认行为差异
XML注入是否可被利用,很大程度上取决于解析器的默认配置。同样是解析XML,不同语言、不同库的安全默认值差异极大,这也是代码审计时必须逐个确认的点。
在PHP中,早年的DOMDocument和SimpleXML默认会解析外部实体,开发者必须显式调用libxml_disable_entity_loader(true)来禁用实体加载。值得注意的是,PHP 8.0之后该函数已被废弃,因为libxml在2.9版本起默认禁用了实体替换,但旧版本环境依然大量存在,审计时不能掉以轻心:
$dom = new DOMDocument();
// PHP 8 之前的版本需要显式禁用外部实体加载
if (function_exists('libxml_disable_entity_loader')) {
libxml_disable_entity_loader(true);
}
$dom->loadXML($xmlString, LIBXML_NONET | LIBXML_NOENT);这里还有个细节:如果显式传入了LIBXML_NOENT标志,即使新版本PHP也会展开实体,等于主动打开了攻击面。因此在审查PHP代码时,看到LIBXML_NOENT就要提高警惕。
在Java体系中,问题更为普遍。Java自带的SAX、DOM解析器以及大量第三方库(如DocumentBuilderFactory)历史上默认支持外部实体。著名的Apache Solr、WebLogic系列CVE漏洞很多都源于XXE。正确的加固方式是在工厂类上逐项禁用:
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// 禁用DTD处理,从根源上阻断XXE
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
// 禁用外部实体和外部参数实体
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
// 禁用XInclude
factory.setXIncludeAware(false);
factory.setExpandEntityReferences(false);
DocumentBuilder builder = factory.newDocumentBuilder();Python的第三方库lxml默认拒绝解析外部实体,相对安全,但标准库中的xml.etree.ElementTree在某些Python版本上存在实体处理问题,而from xml.dom.minidom相关的老代码更是重灾区。最稳妥的做法是统一使用lxml,并显式构造禁用实体的解析器对象:
from lxml import etree parser = etree.XMLParser(resolve_entities=False, no_network=True) tree = etree.fromstring(xml_bytes, parser)
.NET平台下,4.5.2之前的System.Xml.XmlDocument默认允许外部实体,需要设置XmlResolver = null或者使用XmlSecureResolver进行限制。Go语言的encoding/xml标准库则从设计上就不支持外部实体,安全度较高,但如果引入了第三方封装库仍需逐一确认。
完整的防御方案与落地实践
防御XML注入需要从数据流向的多个环节入手,单一措施往往不够,建议组合使用以下手段。
第一层防线是输入校验与转义。凡是用户输入要进入XML文档的地方,必须对XML特殊字符进行转义或使用CDATA包裹,五个关键字符是<、>、&、单引号和双引号,分别转义为<、>、&、'和"。更推荐的做法是完全不手动拼接XML,改用解析器提供的API按节点构建文档:
from lxml import etree
root = etree.Element("order")
etree.SubElement(root, "user").text = user_input
etree.SubElement(root, "remark").text = remark_input
xml_bytes = etree.tostring(root, encoding="UTF-8")这样无论用户输入什么内容,都会被当作纯文本节点处理,从根本上消除了结构注入的可能。这和SQL注入防护中优先使用参数化查询是同一个思路。
第二层防线是解析器加固,也就是前面提到的禁用DTD、外部实体和网络访问。这里可以总结一张速查表供快速核对:
| 环节 | 措施 | 目的 |
|---|---|---|
| 文档构建 | 使用节点API而非字符串拼接 | 阻断结构注入 |
| 输入处理 | 转义五个XML特殊字符 | 兜底防篡改 |
| 解析配置 | 禁止DOCTYPE声明与外部实体 | 阻断XXE |
| 网络层 | 禁用解析器网络访问、出网白名单 | 阻断SSRF转化 |
| 格式选择 | 新接口改用JSON | 减少XML暴露面 |
第三层防线是测试与审计流程。在测试阶段,可以准备一组标准XXE探测载荷,提交给所有接收XML的入口,检查响应中是否出现文件内容或出网请求。常用的验证载荷包括读取系统文件、访问http://127.0.0.1:端口探测端口、以及著名的“十亿笑话攻击”变种——通过嵌套实体指数展开造成内存耗尽的拒绝服务:
<!DOCTYPE lolz [ <!ENTITY lol "lol"> <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;"> <!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;"> <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;"> ]> <root>&lol3;</root>
短短几层嵌套就能展开出数十亿个字符串,直接把解析器内存打爆。这也说明,即便不需要读取文件,仅靠实体展开本身就能构成攻击,禁用DTD是唯一可靠的防护。
最后还需要注意一些容易被忽视的XML入口:Word、Excel等OOXML格式文档本质上是ZIP压缩的XML集合,如果系统提供文档解析或预览功能,恶意文档同样可以携带XXE载荷;SOAP接口的报文体、SAML单点登录的断言、SVG图片上传后的服务端处理,这些都是XML注入的高发地带。安全团队在做资产梳理时,应当把这些间接入口一并纳入扫描范围,而不是只盯着显式的XML接口。
总的来说,XML注入的防护核心就三句话:构建文档时用API不要拼接,解析文档时禁用DTD和外部实体,架构设计上能换JSON就别坚持XML。把这三点落实到编码规范和代码审计清单中,这类漏洞基本就没有生存空间了。