use compliance_core::error::CoreError; use compliance_core::models::dast::{DastEvidence, DastFinding, DastTarget, DastVulnType}; use compliance_core::models::Severity; use compliance_core::traits::dast_agent::{DastAgent, DastContext}; use tracing::{info, warn}; /// SQL Injection testing agent pub struct SqlInjectionAgent { http: reqwest::Client, } impl SqlInjectionAgent { pub fn new(http: reqwest::Client) -> Self { Self { http } } /// Test payloads for SQL injection detection fn payloads(&self) -> Vec<(&str, &str)> { vec![ ("' OR '1'='1", "boolean-based blind"), ("1' AND SLEEP(2)-- -", "time-based blind"), ("' UNION SELECT NULL--", "union-based"), ("1; DROP TABLE test--", "stacked queries"), ("' OR 1=1#", "mysql boolean"), ("1' ORDER BY 1--", "order by probe"), ("') OR ('1'='1", "parenthesis bypass"), ] } /// Error patterns that indicate SQL injection fn error_patterns(&self) -> Vec<&str> { vec![ "sql syntax", "mysql_fetch", "ORA-01756", "SQLite3::query", "pg_query", "unclosed quotation mark", "quoted string not properly terminated", "you have an error in your sql", "warning: mysql", "microsoft sql native client error", "postgresql query failed", "unterminated string", "syntax error at or near", ] } } impl DastAgent for SqlInjectionAgent { fn name(&self) -> &str { "sql_injection" } async fn run( &self, target: &DastTarget, context: &DastContext, ) -> Result, CoreError> { let mut findings = Vec::new(); let target_id = target .id .map(|oid| oid.to_hex()) .unwrap_or_else(|| "unknown".to_string()); for endpoint in &context.endpoints { // Only test endpoints with parameters if endpoint.parameters.is_empty() { continue; } for param in &endpoint.parameters { for (payload, technique) in self.payloads() { // Build the request with the injection payload let test_url = if endpoint.method == "GET" { format!( "{}?{}={}", endpoint.url, param.name, urlencoding::encode(payload) ) } else { endpoint.url.clone() }; let request = if endpoint.method == "POST" { self.http .post(&endpoint.url) .form(&[(param.name.as_str(), payload)]) } else { self.http.get(&test_url) }; let response = match request.send().await { Ok(r) => r, Err(_) => continue, }; let status = response.status().as_u16(); let headers: std::collections::HashMap = response .headers() .iter() .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) .collect(); let body = response.text().await.unwrap_or_default(); // Check for SQL error patterns in response let body_lower = body.to_lowercase(); let is_vulnerable = self .error_patterns() .iter() .any(|pattern| body_lower.contains(pattern)); if is_vulnerable { let snippet = body.chars().take(500).collect::(); let evidence = DastEvidence { request_method: endpoint.method.clone(), request_url: test_url.clone(), request_headers: None, request_body: if endpoint.method == "POST" { Some(format!("{}={}", param.name, payload)) } else { None }, response_status: status, response_headers: Some(headers), response_snippet: Some(snippet), screenshot_path: None, payload: Some(payload.to_string()), response_time_ms: None, }; let mut finding = DastFinding::new( String::new(), // scan_run_id set by orchestrator target_id.clone(), DastVulnType::SqlInjection, format!("SQL Injection ({technique}) in parameter '{}'", param.name), format!( "SQL injection vulnerability detected in parameter '{}' at {} using {} technique. \ The server returned SQL error messages in response to the injected payload.", param.name, endpoint.url, technique ), Severity::Critical, endpoint.url.clone(), endpoint.method.clone(), ); finding.parameter = Some(param.name.clone()); finding.exploitable = true; finding.evidence = vec![evidence]; finding.cwe = Some("CWE-89".to_string()); finding.remediation = Some( "Use parameterized queries or prepared statements. \ Never concatenate user input into SQL queries." .to_string(), ); findings.push(finding); warn!( endpoint = %endpoint.url, param = %param.name, technique, "SQL injection found" ); // Don't test more payloads for same param once confirmed break; } } } } info!(findings = findings.len(), "SQL injection scan complete"); Ok(findings) } } /// URL-encode a string for query parameters mod urlencoding { pub fn encode(input: &str) -> String { let mut encoded = String::new(); for byte in input.bytes() { match byte { b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => { encoded.push(byte as char); } _ => { encoded.push_str(&format!("%{:02X}", byte)); } } } encoded } }