security_rust/injection/
nosql_injection.rs1use 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 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}