security_rust/protocol/
header_injection.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(
11 r"(?i)\r\n\s*(?:Set-Cookie|Location|Content-Length|Content-Type|Transfer-Encoding):",
12 )
13 .unwrap(),
14 Regex::new(r"(?i)%0[dD].*%0[aA]").unwrap(),
15 ]
16});
17
18pub struct HeaderInjectionDetector;
19
20impl Detector for HeaderInjectionDetector {
21 fn name(&self) -> &'static str {
22 "header_injection"
23 }
24
25 fn detect(&self, input: &str) -> Option<DetectionResult> {
26 regex_detect(&PATTERNS, self.name(), AttackCategory::Protocol, Severity::High, "HTTP header injection (CRLF) detected", input)
27 }
28}
29
30#[cfg(test)]
31mod tests {
32 use super::*;
33
34 fn assert_detected(input: &str) {
35 crate::test_helpers::assert_detected(
36 &HeaderInjectionDetector,
37 input,
38 AttackCategory::Protocol,
39 Severity::High,
40 );
41 }
42
43 fn assert_clean(input: &str) {
44 crate::test_helpers::assert_clean(&HeaderInjectionDetector, input);
45 }
46
47 #[test]
48 fn name_is_header_injection() {
49 assert_eq!(HeaderInjectionDetector.name(), "header_injection");
50 }
51
52 #[test]
53 fn detects_encoded_crlf_set_cookie() {
54 assert_detected("test%0d%0aSet-Cookie: evil=true");
55 }
56
57 #[test]
58 fn detects_encoded_crlf_location() {
59 assert_detected("redirect?url=%0D%0ALocation: /admin");
60 }
61
62 #[test]
63 fn detects_encoded_crlf_content_length() {
64 assert_detected("body%0d%0aContent-Length: 0");
65 }
66
67 #[test]
68 fn detects_raw_crlf_headers() {
69 assert_detected("foo\r\nContent-Type: text/html");
70 assert_detected("bar\r\nTransfer-Encoding: chunked");
71 }
72
73 #[test]
74 fn detects_scattered_encoded_crlf() {
75 assert_detected("a%0dcontent%0a");
76 }
77
78 #[test]
79 fn rejects_clean_headers() {
80 assert_clean("Set-Cookie: evil=true");
81 assert_clean("Location: /index.php");
82 assert_clean("Content-Type: text/html");
83 }
84
85 #[test]
86 fn rejects_lone_encoded_chars() {
87 assert_clean("%0d");
88 assert_clean("%0a");
89 assert_clean("test%0dend");
90 assert_clean("%0d%0d");
91 }
92
93 #[test]
94 fn rejects_lf_only_newlines() {
95 assert_clean("foo\nContent-Type: text/html");
96 assert_clean("foo\nLocation: /x");
97 }
98
99 #[test]
100 fn rejects_near_misses() {
101 assert_clean("foo\r\nContent-Type text/html");
102 }
103
104 #[test]
105 fn rejects_empty_and_whitespace() {
106 assert_clean("");
107 assert_clean(" ");
108 assert_clean("\r\n");
109 }
110
111 #[test]
112 fn rejects_unicode_text() {
113 assert_clean("这是一段正常文本,无注入");
114 }
115}