Files
compliance-scanner-agent/compliance-dast/src/agents/ssrf.rs
T
sharangandSharang Parnerkar 42cabf0582
CI / Format (push) Successful in 2s
CI / Clippy (push) Successful in 2m56s
CI / Security Audit (push) Successful in 1m25s
CI / Tests (push) Successful in 3m57s
feat: rag-embedding-ai-chat (#1)
Co-authored-by: Sharang Parnerkar <parnerkarsharang@gmail.com>
Reviewed-on: #1
2026-03-06 21:54:15 +00:00

161 lines
6.0 KiB
Rust

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;
/// Server-Side Request Forgery (SSRF) testing agent
pub struct SsrfAgent {
http: reqwest::Client,
}
impl SsrfAgent {
pub fn new(http: reqwest::Client) -> Self {
Self { http }
}
fn payloads(&self) -> Vec<(&str, &str)> {
vec![
("http://127.0.0.1", "localhost IPv4"),
("http://[::1]", "localhost IPv6"),
("http://0.0.0.0", "zero address"),
("http://169.254.169.254/latest/meta-data/", "AWS metadata"),
("http://metadata.google.internal/", "GCP metadata"),
("http://127.0.0.1:22", "SSH port probe"),
("http://127.0.0.1:3306", "MySQL port probe"),
("http://localhost/admin", "localhost admin"),
]
}
fn internal_indicators(&self) -> Vec<&str> {
vec![
"ami-id",
"instance-id",
"local-hostname",
"public-hostname",
"iam/security-credentials",
"computeMetadata",
"OpenSSH",
"mysql_native_password",
"root:x:0:",
"<!DOCTYPE html>",
]
}
}
impl DastAgent for SsrfAgent {
fn name(&self) -> &str {
"ssrf"
}
async fn run(
&self,
target: &DastTarget,
context: &DastContext,
) -> Result<Vec<DastFinding>, CoreError> {
let mut findings = Vec::new();
let target_id = target
.id
.map(|oid| oid.to_hex())
.unwrap_or_else(|| "unknown".to_string());
// Find endpoints with URL-like parameters
for endpoint in &context.endpoints {
let url_params: Vec<_> = endpoint
.parameters
.iter()
.filter(|p| {
let name_lower = p.name.to_lowercase();
name_lower.contains("url")
|| name_lower.contains("uri")
|| name_lower.contains("link")
|| name_lower.contains("src")
|| name_lower.contains("redirect")
|| name_lower.contains("callback")
|| name_lower.contains("fetch")
|| name_lower.contains("load")
})
.collect();
if url_params.is_empty() {
continue;
}
for param in &url_params {
for (payload, technique) in self.payloads() {
let request = if endpoint.method == "POST" {
self.http
.post(&endpoint.url)
.form(&[(param.name.as_str(), payload)])
} else {
let test_url = format!("{}?{}={}", endpoint.url, param.name, payload);
self.http.get(&test_url)
};
let response = match request.send().await {
Ok(r) => r,
Err(_) => continue,
};
let status = response.status().as_u16();
let body = response.text().await.unwrap_or_default();
// Check for SSRF indicators
let body_lower = body.to_lowercase();
let is_vulnerable = self
.internal_indicators()
.iter()
.any(|indicator| body_lower.contains(&indicator.to_lowercase()));
if is_vulnerable {
let snippet = body.chars().take(500).collect::<String>();
let evidence = DastEvidence {
request_method: endpoint.method.clone(),
request_url: endpoint.url.clone(),
request_headers: None,
request_body: Some(format!("{}={}", param.name, payload)),
response_status: status,
response_headers: None,
response_snippet: Some(snippet),
screenshot_path: None,
payload: Some(payload.to_string()),
response_time_ms: None,
};
let mut finding = DastFinding::new(
String::new(),
target_id.clone(),
DastVulnType::Ssrf,
format!("SSRF ({technique}) via parameter '{}'", param.name),
format!(
"Server-side request forgery detected in parameter '{}' at {}. \
The application made a request to an internal resource ({}).",
param.name, endpoint.url, payload
),
Severity::High,
endpoint.url.clone(),
endpoint.method.clone(),
);
finding.parameter = Some(param.name.clone());
finding.exploitable = true;
finding.evidence = vec![evidence];
finding.cwe = Some("CWE-918".to_string());
finding.remediation = Some(
"Validate and sanitize all user-supplied URLs. \
Use allowlists for permitted domains and block internal IP ranges."
.to_string(),
);
findings.push(finding);
break;
}
}
}
}
info!(findings = findings.len(), "SSRF scan complete");
Ok(findings)
}
}