Why AI tools produce this: AI coding tools generate this anti-pattern by default, it appears in a large share of AI-written auth code.
Why this matters
Because the destination is attacker-controlled, this is a Server-Side Request Forgery (CWE-918): an attacker can point the request at internal services behind your firewall, or at the cloud metadata endpoint (http://169.254.169.254/...) to steal IAM/instance credentials and pivot deeper into your infrastructure.
Never pass a request-derived String (an axum/actix handler parameter, or a value taken from request input) straight into reqwest::get(...) or a Client::get(...) / Client::post(...).send(). Validate the destination host against an explicit allow-list (parse the URL and check the resolved host/scheme, rejecting private/loopback/link-local ranges) before issuing the request.
VULNERABLE
vulnerable.rs
use axum::extract::{Path, Query};use std::collections::HashMap;// Inline: a handler `String` parameter flows straight into reqwest::get.async fn fetch(url: String) -> String { // ruleid: auth.rust.flow.ssrf reqwest::get(url).await.unwrap().text().await.unwrap()}// Indirection: the parameter is assigned to a local, then requested via a// client. Taint tracks through the binding.async fn proxy(target: String, client: reqwest::Client) -> String { let dest = target; // ruleid: auth.rust.flow.ssrf client.get(dest).send().await.unwrap().text().await.unwrap()}// POST against a request-derived URL.async fn relay(endpoint: String, client: reqwest::Client) { // ruleid: auth.rust.flow.ssrf let _ = client.post(endpoint).send().await;}// axum Query extractor destructured to a struct, the inner field flows out.async fn from_query(Query(params): Query<HashMap<String, String>>) -> String { let url = params.get("url").cloned().unwrap_or_default(); // ruleid: auth.rust.flow.ssrf reqwest::get(url).await.unwrap().text().await.unwrap()}// axum Path extractor parameter into a blocking client request.async fn from_path(Path(host): Path<String>, client: reqwest::Client) { // ruleid: auth.rust.flow.ssrf let _ = client.head(host).send().await;}
SAFE
safe.rs
// Outbound HTTP requests that are NOT SSRF — must produce zero findings.fn is_allowed_url(url: &str) -> bool { url.starts_with("https://api.internal.example.com/")}const ALLOWED_URLS: [&str; 1] = ["https://api.internal.example.com/health"];// Safe: a constant, attacker-uncontrolled target (a local, not a parameter).async fn fetch_constant(client: reqwest::Client) -> String { let url = "https://api.internal.example.com/health".to_string(); // ok: auth.rust.flow.ssrf client.get(url).send().await.unwrap().text().await.unwrap()}// Safe: the parameter is only requested inside an allow-list host-validation// guard, which clears the taint.async fn fetch_validated(target: String, client: reqwest::Client) -> String { if is_allowed_url(&target) { // ok: auth.rust.flow.ssrf return client.get(target).send().await.unwrap().text().await.unwrap(); } "rejected".to_string()}// Safe: explicit allow-list membership guard on the exact value used.async fn fetch_guarded(endpoint: String, client: reqwest::Client) -> String { if ALLOWED_URLS.contains(&endpoint.as_str()) { // ok: auth.rust.flow.ssrf return client.get(endpoint).send().await.unwrap().text().await.unwrap(); } "rejected".to_string()}
Suppressing this rule
If a finding is a genuine false positive, scope the suppression to the exact line and leave a reason, never disable the rule project-wide. Disable directives are line-scoped by design.