security_rust/protocol/
header_injection.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(
11 r"(?i)\r\n\s*(?:Set-Cookie|Location|Content-Length|Content-Type|Transfer-Encoding|Refresh|Status|WWW-Authenticate):",
12 )
13 .unwrap(),
14 Regex::new(r"(?i)%0[dD].*%0[aA]").unwrap(),
15 Regex::new(r"(?i)%0[aA].*%0[dD]").unwrap(),
20 ]
21});
22
23pub struct HeaderInjectionDetector;
24
25impl Detector for HeaderInjectionDetector {
26 fn name(&self) -> &'static str {
27 "header_injection"
28 }
29
30 fn detect(&self, input: &str) -> Option<DetectionResult> {
31 regex_detect(
32 &PATTERNS,
33 self.name(),
34 AttackCategory::Protocol,
35 Severity::High,
36 "HTTP header injection (CRLF) detected",
37 input,
38 )
39 }
40}
41
42#[cfg(test)]
43mod tests {
44 use super::*;
45
46 fn assert_detected(input: &str) {
47 crate::test_helpers::assert_detected(
48 &HeaderInjectionDetector,
49 input,
50 AttackCategory::Protocol,
51 Severity::High,
52 );
53 }
54
55 fn assert_clean(input: &str) {
56 crate::test_helpers::assert_clean(&HeaderInjectionDetector, input);
57 }
58
59 #[test]
60 fn name_is_header_injection() {
61 assert_eq!(HeaderInjectionDetector.name(), "header_injection");
62 }
63
64 #[test]
65 fn detects_encoded_crlf_set_cookie() {
66 assert_detected("test%0d%0aSet-Cookie: evil=true");
67 }
68
69 #[test]
70 fn detects_encoded_crlf_location() {
71 assert_detected("redirect?url=%0D%0ALocation: /admin");
72 }
73
74 #[test]
75 fn detects_encoded_crlf_content_length() {
76 assert_detected("body%0d%0aContent-Length: 0");
77 }
78
79 #[test]
80 fn detects_raw_crlf_headers() {
81 assert_detected("foo\r\nContent-Type: text/html");
82 assert_detected("bar\r\nTransfer-Encoding: chunked");
83 }
84
85 #[test]
86 fn detects_scattered_encoded_crlf() {
87 assert_detected("a%0dcontent%0a");
88 }
89
90 #[test]
91 fn detects_reverse_order_encoded_crlf() {
92 assert_detected("%0a%0d");
95 assert_detected("value%0a%0dSet-Cookie: session=evil");
96 assert_detected("a%0Acontent%0D");
97 }
98
99 #[test]
100 fn detects_injection_via_redirect_headers() {
101 assert_detected("value\r\nRefresh: 0;url=http://evil.com");
104 assert_detected("value\r\nStatus: 302");
105 assert_detected("value\r\nWWW-Authenticate: Basic realm=x");
106 }
107
108 #[test]
109 fn rejects_lf_only_prose() {
110 assert_clean("Meeting at 3pm\nlocation: Room 5");
113 assert_clean("笔记\nset-cookie: abc");
114 assert_clean("配置:\nrefresh: 30\nlocation: /var/www");
115 assert_clean("note\ncontent-length: 0");
116 }
117
118 #[test]
119 fn rejects_clean_headers() {
120 assert_clean("Set-Cookie: evil=true");
121 assert_clean("Location: /index.php");
122 assert_clean("Content-Type: text/html");
123 }
124
125 #[test]
126 fn rejects_lone_encoded_chars() {
127 assert_clean("%0d");
128 assert_clean("%0a");
129 assert_clean("test%0dend");
130 assert_clean("%0d%0d");
131 }
132
133 #[test]
134 fn rejects_lf_only_newlines() {
135 assert_clean("foo\nContent-Type: text/html");
136 assert_clean("foo\nLocation: /x");
137 }
138
139 #[test]
140 fn rejects_near_misses() {
141 assert_clean("foo\r\nContent-Type text/html");
142 }
143
144 #[test]
145 fn rejects_empty_and_whitespace() {
146 assert_clean("");
147 assert_clean(" ");
148 assert_clean("\r\n");
149 }
150
151 #[test]
152 fn rejects_unicode_text() {
153 assert_clean("这是一段正常文本,无注入");
154 }
155}