use crate::{AttackCategory, DetectionResult, Detector, Severity, regex_detect};
use regex::Regex;
use std::sync::LazyLock;
static PATTERNS: LazyLock<Vec<Regex>> = LazyLock::new(|| {
vec![
Regex::new(
r"(?i)&[a-z0-9_%.\-]{1,64}=[^&;\s?]{0,200};[a-z0-9_%.\-]{1,64}=[^&;\s?]{0,200}(?:&|$)",
)
.unwrap(),
Regex::new(
r"(?i);[a-z0-9_%.\-]{1,64}=[^&;\s?]{0,200}&[a-z0-9_%.\-]{1,64}=[^&;\s?]{0,200}(?:&|$)",
)
.unwrap(),
]
});
fn has_matrix_session(input: &str) -> bool {
const NEEDLE: &[u8] = b";jsessionid=";
input
.as_bytes()
.windows(NEEDLE.len())
.any(|w| w.eq_ignore_ascii_case(NEEDLE))
}
fn duplicate_key(input: &str) -> Option<(usize, usize)> {
const MAX_TOKENS: usize = 64;
let mut keys: Vec<&str> = Vec::new();
let mut token_start = 0usize;
for (i, token) in input.split(['&', ';']).enumerate() {
let start = token_start;
token_start = start + token.len() + 1; if i >= MAX_TOKENS {
break;
}
let Some(eq) = token.find('=') else {
continue;
};
let raw = &token[..eq];
let key_head = raw.rfind(['?', '#']).map_or(0, |p| p + 1);
let tail = &raw[key_head..];
let key = tail.trim();
if key.is_empty() {
continue;
}
if keys.contains(&key) {
let lead = tail.len() - tail.trim_start().len();
let at = start + key_head + lead;
return Some((at, key.len()));
}
keys.push(key);
}
None
}
pub struct HttpParameterPollutionDetector;
impl Detector for HttpParameterPollutionDetector {
fn name(&self) -> &'static str {
"hpp"
}
fn detect(&self, input: &str) -> Option<DetectionResult> {
if let Some((offset, len)) = duplicate_key(input) {
return Some(DetectionResult {
attack_type: self.name().to_string(),
category: AttackCategory::Protocol,
severity: Severity::Medium,
matched_pattern: input[offset..offset + len].to_string(),
offset,
message: "HTTP parameter pollution detected".into(),
});
}
if has_matrix_session(input) {
return None;
}
regex_detect(
&PATTERNS,
self.name(),
AttackCategory::Protocol,
Severity::Medium,
"HTTP parameter pollution detected",
input,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::{assert_clean, assert_detected};
fn det() -> HttpParameterPollutionDetector {
HttpParameterPollutionDetector
}
fn assert_hit(input: &str) {
assert_detected(&det(), input, AttackCategory::Protocol, Severity::Medium);
}
#[test]
fn name_is_hpp() {
assert_eq!(det().name(), "hpp");
}
#[test]
fn detects_repeated_key() {
for input in [
"a=1&a=2",
"?id=1&id=2",
"id=1&id=2&id=3",
"user=admin&user=guest",
"a=1&b=2&a=3",
] {
assert_hit(input);
}
}
#[test]
fn detects_repeated_array_key() {
for input in [
"a[]=1&a[]=2",
"ids[]=1&ids[]=2&ids[]=3",
"?filter[role]=admin&filter[role]=user",
] {
assert_hit(input);
}
}
#[test]
fn detects_mixed_delimiters() {
for input in [
"a=1&b=2;c=3",
"a=1;b=2&c=3",
"?x=1&y=2;z=3",
"a=1&b=2;c=3&d=4",
"a=1;b=2&c=3&d=4",
] {
assert_hit(input);
}
}
#[test]
fn ignores_distinct_keys() {
for input in [
"a=1&b=2",
"?page=2&size=20&sort=name",
"user=admin&pass=123",
"a=1&b=2&c=3&d=4",
] {
assert_clean(&det(), input);
}
}
#[test]
fn ignores_matrix_parameter_and_request_line() {
assert_clean(&det(), "http://x.com/app;jsessionid=ABC123?p=1&q=2");
assert_clean(&det(), "http://x.com/app;JSESSIONID=ABC123?p=1&q=2");
assert_clean(&det(), "http://x.com/app;jsessionid=ABC123&p=1");
assert_clean(&det(), "GET /a?x=1&y=2;z=3 HTTP/1.1");
assert_clean(&det(), "a=1&b=2;c=3 说明");
}
#[test]
fn ignores_benign_inputs() {
for input in [
"Hello, this is a normal text input. Nothing suspicious here.",
"q=donation=5",
"q=2024--2025",
"a=1;b=2",
"Content-Type: multipart/mixed; boundary=abc123",
"1 & 1 == 2",
"a + b; c + d",
"name=John Doe",
"穿越之霸道总裁爱上我--重生之都市修仙",
] {
assert_clean(&det(), input);
}
}
#[test]
fn edge_cases() {
assert_clean(&det(), "");
assert_clean(&det(), " ");
assert_clean(&det(), "&");
assert_clean(&det(), "&=");
assert_clean(&det(), "=1&=2");
assert_clean(&det(), "a=1&a");
assert_hit("?id=1&id=2");
assert_hit("/p?id=1&id=2");
assert_hit("http://h/p?id=1&id=2");
assert_hit("a=&a=");
let r = det().detect("?x=1&y=2&x=3").expect("expected detection");
assert_eq!(
&"?x=1&y=2&x=3"[r.offset..r.offset + r.matched_pattern.len()],
r.matched_pattern
);
assert_eq!(r.matched_pattern, "x");
}
}