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