XXE 外部实体注入防御实战:从原理到防护配置

适用场景

本文适用于所有解析 XML 数据的 Web 应用与接口,包括:SOAP 接口、微信/支付宝回调报文解析、OA 系统数据交换、旧版 Office 文档解析、SVG 上传处理等。当应用直接使用底层 XML 解析器(DOM / SAX / StAX)处理用户可控 XML 而未做安全配置时,即存在 XXE 外部实体注入风险。

前置条件

  • 已定位应用使用的 XML 解析技术栈(Java / Python / PHP 等)
  • 具备对应服务代码修改权限与 Nginx 配置权限
  • 测试环境可复现:准备一个可回显的 XML 解析接口用于验证

原理说明

XML 规范允许通过 DTD(Document Type Definition) 定义实体,其中 外部实体 可引用外部资源:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>&xxe;</root>

若解析器默认加载 DTD 并解析外部实体,&xxe; 会被替换为 /etc/passwd 内容,造成任意文件读取;将协议换为 http:// 则形成 SSRF;结合 expect://jar:// 等协议还可执行命令(PHP 环境)。XXE 长期位列 OWASP Top 10,根因是解析器对不可信 XML 默认启用了 DTD 外部实体加载。

操作步骤

步骤一:Java 解析器禁用外部实体

以最常用的 DocumentBuilderFactory 为例(JDK 7+ 推荐方式):

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
// 禁用 DTD 与外部实体(关键配置)
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);

使用 SAXParserFactoryTransformerFactoryXMLReader 的代码需按同样方式逐一设置。强烈建议封装统一的 SafeXmlParser 工具类,避免各业务各自实现。

步骤二:Python 解析器使用 defusedxml

pip install defusedxml

# 替代标准库 xml.etree.ElementTree 的用法
from defusedxml.ElementTree import fromstring
from defusedxml import minidom, sax

root = fromstring(xml_data)  # 默认拦截所有实体攻击

使用 lxml 时显式禁用:

from lxml import etree
parser = etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False)
root = etree.fromstring(xml_data, parser=parser)

步骤三:PHP 解析器禁用外部实体加载

// PHP 7.2+:libxml_disable_entity_loader 已废弃,改为 libxml 2.9+ 默认安全
// 使用 DOMDocument 时显式禁用:
$dom = new DOMDocument();
$dom->loadXML($xml, LIBXML_NONET | LIBXML_NOENT);  // 注意:LIBXML_NOENT 会展开实体,勿用
// 正确方式:
$dom->loadXML($xml, LIBXML_NONET | LIBXML_NOERROR);

// SimpleXML 场景
$simplexml = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NONET);

注意:libxml 2.9+ 默认不加载外部实体,但仍建议显式传 LIBXML_NONET 关闭网络访问。

步骤四:Nginx 层 WAF 拦截

# /etc/nginx/conf.d/xxe-waf.conf
location /xml/ {
    # 拦截 DOCTYPE 声明
    if ($request_body ~* "<!DOCTYPE" ) { return 403; }
    # 拦截实体声明与危险协议
    if ($request_body ~* "<!ENTITY|SYSTEM|file://|expect://|php://") {
        return 403;
    }
    proxy_pass http://backend;
}
nginx -t && nginx -s reload

步骤五:服务端出口流量管控

即使解析器配置遗漏,也可通过防火墙阻断服务器对外 http/https/ftp 主动连接(除必要业务白名单外),从网络层掐断 XXE 的 数据外带 通道。

配置验证

# 1. 构造测试载荷(读取 /etc/passwd)
cat > xxe_test.xml <<'EOF'
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<root>&xxe;</root>
EOF

# 2. 提交到解析接口,返回内容中不应出现 root: 等系统文件内容
curl -X POST http://your-site.com/xml/parse -H "Content-Type: application/xml" --data @xxe_test.xml

# 3. 验证 WAF 拦截:应返回 403
curl -X POST http://your-site.com/xml/parse -H "Content-Type: application/xml"   -d '<?xml version="1.0"?><!DOCTYPE foo [<!ENTITY x SYSTEM "file:///etc/passwd">]><r>&x;</r>' -w "HTTP %{http_code}
"

常见问题

Q1:业务 XML 中确实需要 DTD 或实体功能,禁用后解析失败?

分场景处理:① 实体仅用于内部复用且数据可信,可保持解析器默认行为,但在 XML 来源处强制白名单校验(仅允许内部系统调用);② 数据不可信场景一律禁用 DTD;③ 若需要自定义实体且来源受限,可用 setEntityResolver 提供本地解析器替代外部加载。

Q2:配置后接口偶发超时或回显乱码?

常见原因:XML 声明中的编码与提交内容不符(如声明 encoding="GBK" 但实际 UTF-8);或残留的 XInclude 处理被关闭后引用失败。建议统一使用 UTF-8 编码,并在 setXIncludeAware(false) 后确认业务无 XInclude 依赖。

总结

XXE 防御的核心只有一条:对不可信 XML 一律禁用 DTD 与外部实体解析。本文给出了 Java、Python、PHP 三大主流技术栈的解析器安全配置、Nginx 流量层拦截规则以及服务端出口管控三层方案。建议在 CI 流水线中加入 XXE 自动化测试用例,将防护固化到开发流程中,防止新代码回归。