bext-waf 0.2.0

Web Application Firewall for bext — rate limiting, IP filtering, GeoIP, rule engine
Documentation
//! NoSQL injection detection (MongoDB, CouchDB, etc.).
//!
//! Detects `$gt`, `$ne`, `$where`, `$regex` operators and JavaScript
//! expressions in query strings and JSON bodies.

use std::sync::OnceLock;

use regex::RegexSet;

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

static NOSQL_DESCRIPTIONS: &[&str] = &[
    "NoSQL operator injection ($gt/$ne/$lt/$gte/$lte)",
    "NoSQL $where / $regex injection",
    "NoSQL $or/$and/$nor array injection",
    "MongoDB $lookup / $unionWith aggregation abuse",
];

fn patterns() -> &'static RegexSet {
    NOSQL_PATTERNS.get_or_init(|| {
        RegexSet::new([
            // 0: Comparison operators in JSON context
            r#"(?i)"\$(gt|gte|lt|lte|ne|eq|in|nin)"\s*:"#,
            // 1: $where / $regex (code execution)
            r#"(?i)"\$(where|regex|expr|text|mod)"\s*:"#,
            // 2: Logical operators as injection
            r#"(?i)"\$(or|and|nor|not)"\s*:\s*\["#,
            // 3: Aggregation abuse
            r#"(?i)"\$(lookup|unionWith|merge|out|graphLookup)"\s*:"#,
        ])
        .expect("NoSQL regex patterns must compile")
    })
}

/// Check an input string for NoSQL injection patterns.
pub fn check_nosql(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(
            NOSQL_DESCRIPTIONS
                .get(idx)
                .unwrap_or(&"NoSQL injection")
                .to_string(),
        )
    }
}

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

    #[test]
    fn detects_comparison_operators() {
        assert!(check_nosql(r#"{"username":"admin","password":{"$ne":""}}"#).is_some());
        assert!(check_nosql(r#"{"age":{"$gt":0}}"#).is_some());
    }

    #[test]
    fn detects_where_regex() {
        assert!(check_nosql(r#"{"$where":"this.password.match(/admin/)"}"#).is_some());
        assert!(check_nosql(r#"{"name":{"$regex":"^admin"}}"#).is_some());
    }

    #[test]
    fn detects_logical_operators() {
        assert!(check_nosql(r#"{"$or":[{"user":"admin"},{"user":"root"}]}"#).is_some());
    }

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

    #[test]
    fn allows_normal_text() {
        assert!(check_nosql("Hello world").is_none());
    }
}