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; /// Authentication bypass testing agent pub struct AuthBypassAgent { http: reqwest::Client, } impl AuthBypassAgent { pub fn new(http: reqwest::Client) -> Self { Self { http } } } impl DastAgent for AuthBypassAgent { fn name(&self) -> &str { "auth_bypass" } 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()); // Test 1: Access protected endpoints without authentication for endpoint in &context.endpoints { if !endpoint.requires_auth { continue; } // Try accessing without auth let response = match self.http.get(&endpoint.url).send().await { Ok(r) => r, Err(_) => continue, }; let status = response.status().as_u16(); // If we get 200 on a supposedly auth-required endpoint if status == 200 { let body = response.text().await.unwrap_or_default(); let snippet = body.chars().take(500).collect::(); let evidence = DastEvidence { request_method: "GET".to_string(), request_url: endpoint.url.clone(), request_headers: None, request_body: None, response_status: status, response_headers: None, response_snippet: Some(snippet), screenshot_path: None, payload: None, response_time_ms: None, }; let mut finding = DastFinding::new( String::new(), target_id.clone(), DastVulnType::AuthBypass, format!("Authentication bypass on {}", endpoint.url), format!( "Protected endpoint {} returned HTTP 200 without authentication credentials.", endpoint.url ), Severity::Critical, endpoint.url.clone(), "GET".to_string(), ); finding.exploitable = true; finding.evidence = vec![evidence]; finding.cwe = Some("CWE-287".to_string()); finding.remediation = Some( "Ensure all protected endpoints validate authentication tokens. \ Implement server-side authentication checks that cannot be bypassed." .to_string(), ); findings.push(finding); } } // Test 2: HTTP method tampering let methods = ["PUT", "PATCH", "DELETE", "OPTIONS"]; for endpoint in &context.endpoints { if endpoint.method != "GET" && endpoint.method != "POST" { continue; } for method in &methods { let response = match self .http .request( reqwest::Method::from_bytes(method.as_bytes()) .unwrap_or(reqwest::Method::GET), &endpoint.url, ) .send() .await { Ok(r) => r, Err(_) => continue, }; let status = response.status().as_u16(); // If a non-standard method returns 200 when it shouldn't if status == 200 && *method == "DELETE" && !target.allow_destructive { let evidence = DastEvidence { request_method: method.to_string(), request_url: endpoint.url.clone(), request_headers: None, request_body: None, response_status: status, response_headers: None, response_snippet: None, screenshot_path: None, payload: None, response_time_ms: None, }; let mut finding = DastFinding::new( String::new(), target_id.clone(), DastVulnType::AuthBypass, format!( "HTTP method tampering: {} accepted on {}", method, endpoint.url ), format!( "Endpoint {} accepts {} requests which may bypass access controls.", endpoint.url, method ), Severity::Medium, endpoint.url.clone(), method.to_string(), ); finding.evidence = vec![evidence]; finding.cwe = Some("CWE-288".to_string()); findings.push(finding); } } } // Test 3: Path traversal for auth bypass let traversal_paths = [ "/../admin", "/..;/admin", "/%2e%2e/admin", "/admin%00", "/ADMIN", "/Admin", ]; for path in &traversal_paths { let test_url = format!("{}{}", target.base_url.trim_end_matches('/'), path); let response = match self.http.get(&test_url).send().await { Ok(r) => r, Err(_) => continue, }; let status = response.status().as_u16(); if status == 200 { let body = response.text().await.unwrap_or_default(); // Check if response looks like an admin page let body_lower = body.to_lowercase(); if body_lower.contains("admin") || body_lower.contains("dashboard") || body_lower.contains("management") { let snippet = body.chars().take(500).collect::(); let evidence = DastEvidence { request_method: "GET".to_string(), request_url: test_url.clone(), request_headers: None, request_body: None, response_status: status, response_headers: None, response_snippet: Some(snippet), screenshot_path: None, payload: Some(path.to_string()), response_time_ms: None, }; let mut finding = DastFinding::new( String::new(), target_id.clone(), DastVulnType::AuthBypass, format!("Path traversal auth bypass: {path}"), format!( "Possible authentication bypass via path traversal. \ Accessing '{}' returned admin-like content.", test_url ), Severity::High, test_url, "GET".to_string(), ); finding.evidence = vec![evidence]; finding.cwe = Some("CWE-22".to_string()); findings.push(finding); break; } } } info!(findings = findings.len(), "Auth bypass scan complete"); Ok(findings) } }