security_rust/protocol/
request_smuggling.rs1use regex::Regex;
4use std::sync::LazyLock;
5
6use crate::{AttackCategory, DetectionResult, Detector, Severity, regex_detect};
7
8static PATTERNS: LazyLock<Regex> = LazyLock::new(|| {
24 Regex::new(r"(?i)Transfer-Encoding\s*:.*\r\n.*Transfer-Encoding\s*:|Transfer-Encoding\s+:")
25 .unwrap()
26});
27
28pub struct RequestSmugglingDetector;
29
30impl Detector for RequestSmugglingDetector {
31 fn name(&self) -> &'static str {
32 "request_smuggling"
33 }
34
35 fn detect(&self, input: &str) -> Option<DetectionResult> {
36 regex_detect(
37 std::slice::from_ref(&*PATTERNS),
38 self.name(),
39 AttackCategory::Protocol,
40 Severity::High,
41 "HTTP request smuggling detected",
42 input,
43 )
44 }
45}
46
47#[cfg(test)]
48mod tests {
49 use super::*;
50
51 fn assert_detected(input: &str) {
52 crate::test_helpers::assert_detected(
53 &RequestSmugglingDetector,
54 input,
55 AttackCategory::Protocol,
56 Severity::High,
57 );
58 }
59
60 fn assert_clean(input: &str) {
61 crate::test_helpers::assert_clean(&RequestSmugglingDetector, input);
62 }
63
64 #[test]
65 fn name_is_request_smuggling() {
66 assert_eq!(RequestSmugglingDetector.name(), "request_smuggling");
67 }
68
69 #[test]
70 fn detects_duplicate_transfer_encoding() {
71 assert_detected("Transfer-Encoding: chunked\r\nTransfer-Encoding: identity");
72 assert_detected("Transfer-Encoding : chunked\r\nTransfer-Encoding : identity");
74 assert_detected("Transfer-Encoding : chunked\r\nTransfer-Encoding: identity");
75 }
76
77 #[test]
78 fn detects_obfuscated_transfer_encoding() {
79 assert_detected("Transfer-Encoding : chunked");
82 assert_detected("transfer-encoding : CHUNKED");
83 assert_detected("Transfer-Encoding\t: chunked");
84 }
85
86 #[test]
87 fn rejects_well_formed_transfer_encoding() {
88 for input in [
91 "Transfer-Encoding: chunked",
92 "Transfer-Encoding:chunked",
93 "Transfer-Encoding:\tchunked",
94 "transfer-encoding: CHUNKED",
95 "POST /upload HTTP/1.1\r\nHost: example.com\r\nContent-Type: multipart/form-data; boundary=----x\r\nContent-Length: 1234\r\nTransfer-Encoding: chunked\r\n\r\n",
96 ] {
97 assert_clean(input);
98 }
99 }
100
101 #[test]
102 fn rejects_benign_headers() {
103 assert_clean("Content-Length: 5\r\nContent-Length: 10");
104 assert_clean("Transfer-Encoding: gzip");
105 assert_clean("Connection: keep-alive");
106 }
107
108 #[test]
109 fn rejects_missing_colon() {
110 assert_clean("Transfer-Encoding chunked");
111 }
112
113 #[test]
114 fn rejects_near_misses() {
115 assert_clean("Transfer-Encoding: chuncked");
116 }
117
118 #[test]
120 fn rejects_real_world_text() {
121 assert_clean("The report was generated with SYSTEM \"production\" settings.");
122 assert_clean("Please cc: ops@example.com on the reply.");
123 assert_clean("/search?tag=rust&tag=security&page=2");
124 assert_clean(concat!(
125 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
126 "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" ",
127 "\"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n",
128 "<svg xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M4 4h16v16H4z\"/></svg>"
129 ));
130 }
131
132 #[test]
133 fn rejects_empty_and_whitespace() {
134 assert_clean("");
135 assert_clean(" ");
136 }
137
138 #[test]
139 fn rejects_unicode_text() {
140 assert_clean("普通请求体,无攻击特征");
141 }
142}