use waf_core::{Config, Decision, Phase, RequestContext, WafModule};
#[derive(Default)]
pub struct RequestSmugglingModule {
enabled: bool,
}
impl RequestSmugglingModule {
pub fn new() -> Self {
Self::default()
}
}
fn reject(reason: &'static str) -> Decision {
Decision::Reject {
rule_id: "request-smuggling".to_string(),
reason: reason.to_string(),
status: 400,
retry_after: None,
}
}
fn is_valid_content_length(v: &str) -> bool {
!v.is_empty() && v.bytes().all(|b| b.is_ascii_digit())
}
impl WafModule for RequestSmugglingModule {
fn id(&self) -> &str {
"request_smuggling"
}
fn phase(&self) -> Phase {
Phase::Connection
}
fn init(&mut self, cfg: &Config) {
self.enabled = cfg.modules.request_smuggling.enabled;
}
fn inspect(&self, ctx: &RequestContext) -> Decision {
if !self.enabled {
return Decision::Allow;
}
let cl: Vec<&str> = ctx
.headers
.iter()
.filter(|(n, _)| n == "content-length")
.map(|(_, v)| v.as_str())
.collect();
let te: Vec<&str> = ctx
.headers
.iter()
.filter(|(n, _)| n == "transfer-encoding")
.map(|(_, v)| v.as_str())
.collect();
if !cl.is_empty() && !te.is_empty() {
return reject("content-length and transfer-encoding both present");
}
if !cl.is_empty() {
if cl.len() > 1 {
return reject("duplicate content-length headers");
}
if !is_valid_content_length(cl[0]) {
return reject("malformed content-length value");
}
}
if !te.is_empty() {
if te.len() > 1 {
return reject("duplicate transfer-encoding headers");
}
if !te[0].eq_ignore_ascii_case("chunked") {
return reject("obfuscated or non-chunked transfer-encoding");
}
}
Decision::Allow
}
}