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