use regex::Regex;
use std::sync::LazyLock;
use crate::{AttackCategory, DetectionResult, Detector, Severity, regex_detect};
static PATTERNS: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)Transfer-Encoding\s*:.*\r\n.*Transfer-Encoding\s*:|Transfer-Encoding\s+:")
.unwrap()
});
pub struct RequestSmugglingDetector;
impl Detector for RequestSmugglingDetector {
fn name(&self) -> &'static str {
"request_smuggling"
}
fn detect(&self, input: &str) -> Option<DetectionResult> {
regex_detect(
std::slice::from_ref(&*PATTERNS),
self.name(),
AttackCategory::Protocol,
Severity::High,
"HTTP request smuggling detected",
input,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_detected(input: &str) {
crate::test_helpers::assert_detected(
&RequestSmugglingDetector,
input,
AttackCategory::Protocol,
Severity::High,
);
}
fn assert_clean(input: &str) {
crate::test_helpers::assert_clean(&RequestSmugglingDetector, input);
}
#[test]
fn name_is_request_smuggling() {
assert_eq!(RequestSmugglingDetector.name(), "request_smuggling");
}
#[test]
fn detects_duplicate_transfer_encoding() {
assert_detected("Transfer-Encoding: chunked\r\nTransfer-Encoding: identity");
assert_detected("Transfer-Encoding : chunked\r\nTransfer-Encoding : identity");
assert_detected("Transfer-Encoding : chunked\r\nTransfer-Encoding: identity");
}
#[test]
fn detects_obfuscated_transfer_encoding() {
assert_detected("Transfer-Encoding : chunked");
assert_detected("transfer-encoding : CHUNKED");
assert_detected("Transfer-Encoding\t: chunked");
}
#[test]
fn rejects_well_formed_transfer_encoding() {
for input in [
"Transfer-Encoding: chunked",
"Transfer-Encoding:chunked",
"Transfer-Encoding:\tchunked",
"transfer-encoding: CHUNKED",
"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",
] {
assert_clean(input);
}
}
#[test]
fn rejects_benign_headers() {
assert_clean("Content-Length: 5\r\nContent-Length: 10");
assert_clean("Transfer-Encoding: gzip");
assert_clean("Connection: keep-alive");
}
#[test]
fn rejects_missing_colon() {
assert_clean("Transfer-Encoding chunked");
}
#[test]
fn rejects_near_misses() {
assert_clean("Transfer-Encoding: chuncked");
}
#[test]
fn rejects_real_world_text() {
assert_clean("The report was generated with SYSTEM \"production\" settings.");
assert_clean("Please cc: ops@example.com on the reply.");
assert_clean("/search?tag=rust&tag=security&page=2");
assert_clean(concat!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
"<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" ",
"\"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n",
"<svg xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M4 4h16v16H4z\"/></svg>"
));
}
#[test]
fn rejects_empty_and_whitespace() {
assert_clean("");
assert_clean(" ");
}
#[test]
fn rejects_unicode_text() {
assert_clean("普通请求体,无攻击特征");
}
}