Skip to main content

security_rust/protocol/
header_injection.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2
3use 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        // 反序对 LF-CR。部分解析器容忍这个顺序,是响应拆分的绕过变体。
16        // 编码形态在正常文本里不出现,误报面与上面那条同量级。
17        // (原先由 crlf_injection 覆盖,该检测器因 LF-only 分支误报、其余形态
18        // 与本检测器重复而删除,其独有的这几条形态改由这里接手。)
19        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        // 反序对 LF-CR:部分解析器容忍,是响应拆分的绕过变体。
93        // 这组形态原先由已删除的 crlf_injection 覆盖,现在归本检测器。
94        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        // Refresh / Status / WWW-Authenticate 同样可用于响应拆分,
102        // 原先只在 crlf_injection 的裸 \r\n 名录里,现已并入本检测器
103        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        // 这几条正是旧 crlf_injection 被删除的原因:只认 \r\n,不认裸 \n,
111        // 否则任何多行文本 / YAML / 日志粘贴都会被判 High。此测试防止回归。
112        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}