概念概述
XML 外部实体注入(XXE)(XML External Entity Injection)指的是:XXE 滥用会解析外部实体的 XML 解析器,让攻击者通过特制文档读取本地文件或访问内部服务,涵盖任何解析不可信 XML 的服务。
从更严格的定义看,XML external entity injection is an attack in which a document declares an entity that points to an external resource, causing a misconfigured parser to fetch local files or network endpoints. 该概念在工程实践中的价值,在于它能帮助团队用统一的语言描述一个具体的质量属性或设计约束,从而在评审与协作中减少歧义。
为什么需要它
It affects any service that parses XML from untrusted input, including SOAP endpoints, document converters, and SAML consumers. 它与 NoSQL Injection、LDAP Injection、HTTP Response Splitting 等概念经常一起出现,用于解决相近的工程问题。理解它的适用场景,有助于在多种备选方案之间做出有依据的取舍,而不是凭直觉选择技术方案。
常见误解
- modern XML parsers are safe by default
- XXE only allows reading files and cannot cause server-side requests
需要避免的反模式
- enabling external entity resolution to support a legacy document format
- parsing user-supplied XML with a general-purpose default configuration
实践要点
- Entities can reference file URLs or remote URLs, so a vulnerable parser may disclose file contents in an error or response and can be used for server-side request forgery.
- The reliable mitigation is to disable external entities and DTD processing entirely unless a document type genuinely requires them.
- Billion laughs and similar entity expansion attacks share the same configuration surface, so hardening the parser addresses both disclosure and denial of service.
参考来源
https://owasp.org/www-community/vulnerabilities/XML_External_Entity_(XXE)_Processing
代码示例
-- 危险示范:拼接用户输入(永远不要这样做)
-- SELECT * FROM products WHERE name = '克" OR "1"="1'
-- ORDER BY 攻击同理:排序字段直接内插
-- 安全形态:值全部走绑定参数
SELECT id, name, price_cents
FROM products
WHERE category = $1
AND price_cents BETWEEN $2 AND $3
ORDER BY price_cents
LIMIT $4;
-- 绑定:$1='electronics', $2=1000, $3=5000, $4=20
-- IN 列表的动态占位符:结构生成、值绑定
SELECT id, name
FROM products
WHERE id IN ($1, $2, $3);
-- 占位符数量由应用层数组长度生成,值逐个绑定
-- ORDER BY 无法参数化:白名单映射是唯一出路
-- 应用层伪代码:
-- allowed = { 'price': 'price_cents',
-- 'name': 'name',
-- 'created': 'created_at DESC' }
-- order_sql = allowed[user_input] or fail()
SELECT id, name, price_cents
FROM products
WHERE category = $1
ORDER BY price_cents DESC NULLS LAST; -- order_sql 来自白名单
-- 标识符必须动态时用格式化函数加引号
SELECT quote_ident('user_input_column'); -- 带引号的安全标识符
import xml.etree.ElementTree as ET
from collections.abc import Iterator
from pathlib import Path
def iter_records(path: Path, tag: str = "record") -> Iterator[dict[str, str]]:
context = ET.iterparse(path, events=("end",))
for _, element in context:
if element.tag != tag:
continue
record = {
child.tag: (child.text or "").strip() for child in element
}
yield record
element.clear()
def write_fixture(path: Path) -> None:
path.write_text(
"<catalog>"
"<record><id>1</id><name>Ada</name></record>"
"<record><id>2</id><name>Grace</name></record>"
"</catalog>",
encoding="utf-8",
)
if __name__ == "__main__":
fixture = Path("catalog.xml")
write_fixture(fixture)
try:
print(list(iter_records(fixture)))
finally:
fixture.unlink()