Skip to main content

security_rust/injection/
nosql_injection.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2
3use crate::{regex_detect, AttackCategory, DetectionResult, Detector, Severity};
4use regex::Regex;
5use std::sync::LazyLock;
6
7static PATTERNS: LazyLock<Vec<Regex>> = LazyLock::new(|| {
8    vec![
9        Regex::new(r#""(?i)\$ne"\s*:"#).unwrap(),
10        Regex::new(r#"'(?i)\$ne'\s*:"#).unwrap(),
11        Regex::new(r#""(?i)\$gt"\s*:"#).unwrap(),
12        Regex::new(r#""(?i)\$gte"\s*:"#).unwrap(),
13        Regex::new(r#""(?i)\$lt"\s*:"#).unwrap(),
14        Regex::new(r#""(?i)\$lte"\s*:"#).unwrap(),
15        Regex::new(r#""(?i)\$regex"\s*:"#).unwrap(),
16        Regex::new(r#""(?i)\$where"\s*:"#).unwrap(),
17        Regex::new(r#""(?i)\$or"\s*:"#).unwrap(),
18        Regex::new(r"(?i)\$eq").unwrap(),
19        Regex::new(r"(?i)\$nin").unwrap(),
20        Regex::new(r#"\{\s*"\$gt"\s*:\s*""\s*\}"#).unwrap(),
21    ]
22});
23
24pub struct NoSqlInjectionDetector;
25
26impl Detector for NoSqlInjectionDetector {
27    fn name(&self) -> &'static str {
28        "nosql_injection"
29    }
30
31    fn detect(&self, input: &str) -> Option<DetectionResult> {
32        regex_detect(&PATTERNS, self.name(), AttackCategory::Injection, Severity::Critical, "NoSQL injection detected", input)
33    }
34}
35
36#[cfg(test)]
37mod tests {
38    use super::*;
39
40    fn det() -> NoSqlInjectionDetector {
41        NoSqlInjectionDetector
42    }
43
44    fn assert_hit(input: &str) {
45        crate::test_helpers::assert_detected(
46            &det(),
47            input,
48            AttackCategory::Injection,
49            Severity::Critical,
50        );
51    }
52
53    #[test]
54    fn name_is_nosql_injection() {
55        assert_eq!(det().name(), "nosql_injection");
56    }
57
58    #[test]
59    fn detects_common_payloads() {
60        for input in [
61            r#"{"username": {"$ne": ""}}"#,
62            r#"{"$gt": ""}"#,
63            r#"{"user": {"$regex": "^admin"}}"#,
64            r#"{"$or": [{"role": "admin"}]}"#,
65            r#"{'$ne': ''}"#,
66            r#"{"pass": {"$nin": ["a"]}}"#,
67            r#"db.users.find({"$where": "sleep(5000)"})"#,
68        ] {
69            assert_hit(input);
70        }
71    }
72
73    #[test]
74    fn benign_inputs_not_detected() {
75        for input in [
76            r#"{"name": "John", "age": 30, "city": "New York"}"#,
77            r#"{"price": "$5.99"}"#,
78            "The total cost is $100 and the discount is 10%",
79            "The equation is simple to solve",
80        ] {
81            assert!(det().detect(input).is_none(), "false positive: {input}");
82        }
83    }
84
85    #[test]
86    fn edge_cases() {
87        assert!(det().detect("").is_none());
88        assert!(det().detect("  \t ").is_none());
89        assert!(det().detect("你好世界 こんにちは").is_none());
90        // near misses: operator without colon or without dollar sign
91        assert!(det().detect(r#"{"$ne"}"#).is_none());
92        assert!(det().detect(r#"{"ne": ""}"#).is_none());
93        assert!(det().detect("age > 18").is_none());
94    }
95
96    #[test]
97    fn obfuscated_variants_detected() {
98        for input in [r#"{"$NE": ""}"#, r#"{"$GTE": 5}"#, r#"{"$REGEX": "^a"}"#] {
99            assert_hit(input);
100        }
101    }
102}