bext-waf 0.2.0

Web Application Firewall for bext — rate limiting, IP filtering, GeoIP, rule engine
Documentation
//! Prototype pollution detection.
//!
//! Detects `__proto__`, `constructor.prototype`, and related patterns
//! in request bodies and query strings that can poison JavaScript object
//! prototypes on the server side (Node.js, Deno, Bun).

use std::sync::OnceLock;

use regex::RegexSet;

static PROTO_PATTERNS: OnceLock<RegexSet> = OnceLock::new();

static PROTO_DESCRIPTIONS: &[&str] = &[
    "Prototype pollution: __proto__",
    "Prototype pollution: constructor.prototype",
    "Prototype pollution: Object.assign with __proto__",
];

fn patterns() -> &'static RegexSet {
    PROTO_PATTERNS.get_or_init(|| {
        RegexSet::new([
            // 0: __proto__ in any context
            r"__proto__",
            // 1: constructor.prototype / constructor[prototype]
            r"(?i)constructor\s*[\[.]\s*prototype",
            // 2: Object.assign or merge with __proto__ key
            r#"(?i)"__proto__"\s*:"#,
        ])
        .expect("prototype pollution regex patterns must compile")
    })
}

/// Check an input string for prototype pollution patterns.
pub fn check_prototype(input: &str) -> Option<String> {
    let set = patterns();
    let matches: Vec<_> = set.matches(input).into_iter().collect();
    if matches.is_empty() {
        None
    } else {
        let idx = matches[0];
        Some(
            PROTO_DESCRIPTIONS
                .get(idx)
                .unwrap_or(&"prototype pollution")
                .to_string(),
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn detects_proto() {
        assert!(check_prototype("__proto__[isAdmin]=true").is_some());
        assert!(check_prototype(r#"{"__proto__": {"isAdmin": true}}"#).is_some());
    }

    #[test]
    fn detects_constructor_prototype() {
        assert!(check_prototype("constructor.prototype.isAdmin=true").is_some());
        assert!(check_prototype("constructor[prototype][isAdmin]=1").is_some());
    }

    #[test]
    fn allows_normal_json() {
        assert!(check_prototype(r#"{"name":"John","age":30}"#).is_none());
    }

    #[test]
    fn allows_normal_path() {
        assert!(check_prototype("/api/users/123").is_none());
    }
}