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::{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}