security_rust/data/
mail_header.rs1use crate::{AttackCategory, DetectionResult, Detector, Severity, regex_detect};
4use regex::Regex;
5use std::sync::LazyLock;
6
7static PATTERNS: LazyLock<Vec<Regex>> = LazyLock::new(|| {
8 vec![
9 Regex::new(r"(?i)Bcc\s*:").unwrap(),
10 Regex::new(r"(?i)Cc\s*:").unwrap(),
11 Regex::new(r"(?i)From\s*:.*\r?\n.*From\s*:").unwrap(),
12 Regex::new(r"(?i)MIME-Version\s*:").unwrap(),
13 Regex::new(r"(?i)Content-Type\s*:.*multipart").unwrap(),
14 Regex::new(r"(?i)boundary\s*=").unwrap(),
15 ]
16});
17
18pub struct MailHeaderDetector;
19
20impl Detector for MailHeaderDetector {
21 fn name(&self) -> &'static str {
22 "mail_header"
23 }
24
25 fn detect(&self, input: &str) -> Option<DetectionResult> {
26 regex_detect(
27 &PATTERNS,
28 self.name(),
29 AttackCategory::Data,
30 Severity::Medium,
31 "Mail header injection detected",
32 input,
33 )
34 }
35}
36
37#[cfg(test)]
38mod tests {
39 use super::*;
40
41 #[test]
42 fn name_returns_attack_type() {
43 assert_eq!(MailHeaderDetector.name(), "mail_header");
44 }
45
46 #[test]
47 fn detects_injected_recipient_headers() {
48 for payload in [
49 "Bcc: victim@evil.com",
50 "Cc: victim@evil.com",
51 "bcc: lower@case.com",
52 ] {
53 let r = MailHeaderDetector
54 .detect(payload)
55 .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
56 assert_eq!(r.attack_type, "mail_header");
57 assert_eq!(r.category, AttackCategory::Data);
58 assert_eq!(r.severity, Severity::Medium);
59 assert!(
60 !r.matched_pattern.is_empty(),
61 "matched_pattern empty for {:?}",
62 payload
63 );
64 assert!(
65 r.offset <= payload.len(),
66 "offset out of range for {:?}",
67 payload
68 );
69 }
70 }
71
72 #[test]
73 fn detects_double_from_and_mime_headers() {
74 for payload in [
75 "From: a@b.c\nFrom: c@d.e",
76 "From: a@b.c\r\nFrom: c@d.e",
77 "MIME-Version: 1.0",
78 "Content-Type: multipart/mixed; boundary=abc123",
79 "boundary=abc123",
80 ] {
81 let r = MailHeaderDetector
82 .detect(payload)
83 .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
84 assert!(
85 !r.matched_pattern.is_empty(),
86 "matched_pattern empty for {:?}",
87 payload
88 );
89 assert!(
90 r.offset <= payload.len(),
91 "offset out of range for {:?}",
92 payload
93 );
94 }
95 }
96
97 #[test]
98 fn ignores_benign_inputs() {
99 for input in [
100 "Hello, this is a normal text input.",
101 "Bcc victim@evil.com",
102 "From: a@b.c",
103 "Content-Type: text/plain",
104 "boundary abc",
105 "MIME-Version",
106 "multipart/form-data",
107 ] {
108 assert!(
109 MailHeaderDetector.detect(input).is_none(),
110 "false positive: {:?}",
111 input
112 );
113 }
114 }
115
116 #[test]
117 fn edge_cases() {
118 assert!(MailHeaderDetector.detect("").is_none());
119 assert!(MailHeaderDetector.detect(" ").is_none());
120 assert!(
121 MailHeaderDetector
122 .detect("BCC: evil@example.com")
123 .is_none()
124 ); }
126}