Skip to main content

security_rust/protocol/
request_smuggling.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(r"(?i)Transfer-Encoding:.*\r\n.*Transfer-Encoding:").unwrap(),
11        Regex::new(r"(?i)Transfer-Encoding:[\s]*chunked").unwrap(),
12    ]
13});
14
15pub struct RequestSmugglingDetector;
16
17impl Detector for RequestSmugglingDetector {
18    fn name(&self) -> &'static str {
19        "request_smuggling"
20    }
21
22    fn detect(&self, input: &str) -> Option<DetectionResult> {
23        regex_detect(
24            &PATTERNS,
25            self.name(),
26            AttackCategory::Protocol,
27            Severity::High,
28            "HTTP request smuggling detected",
29            input,
30        )
31    }
32}
33
34#[cfg(test)]
35mod tests {
36    use super::*;
37
38    fn assert_detected(input: &str) {
39        crate::test_helpers::assert_detected(
40            &RequestSmugglingDetector,
41            input,
42            AttackCategory::Protocol,
43            Severity::High,
44        );
45    }
46
47    fn assert_clean(input: &str) {
48        crate::test_helpers::assert_clean(&RequestSmugglingDetector, input);
49    }
50
51    #[test]
52    fn name_is_request_smuggling() {
53        assert_eq!(RequestSmugglingDetector.name(), "request_smuggling");
54    }
55
56    #[test]
57    fn detects_duplicate_transfer_encoding() {
58        assert_detected("Transfer-Encoding: chunked\r\nTransfer-Encoding: identity");
59    }
60
61    #[test]
62    fn detects_chunked_transfer_encoding() {
63        assert_detected("Transfer-Encoding: chunked");
64        assert_detected("Transfer-Encoding:chunked");
65        assert_detected("Transfer-Encoding:\tchunked");
66    }
67
68    #[test]
69    fn detects_mixed_case() {
70        assert_detected("transfer-encoding: CHUNKED");
71    }
72
73    #[test]
74    fn rejects_benign_headers() {
75        assert_clean("Content-Length: 5\r\nContent-Length: 10");
76        assert_clean("Transfer-Encoding: gzip");
77        assert_clean("Connection: keep-alive");
78    }
79
80    #[test]
81    fn rejects_missing_colon() {
82        assert_clean("Transfer-Encoding chunked");
83    }
84
85    #[test]
86    fn rejects_near_misses() {
87        assert_clean("Transfer-Encoding: chuncked");
88    }
89
90    #[test]
91    fn rejects_empty_and_whitespace() {
92        assert_clean("");
93        assert_clean("   ");
94    }
95
96    #[test]
97    fn rejects_unicode_text() {
98        assert_clean("普通请求体,无攻击特征");
99    }
100}