Ssrf Server Side in Axum (Rust)
Ssrf Server Side in Axum with Rust — how this specific combination creates or exposes the vulnerability
Server-side request forgery (SSRF) in an Axum service written in Rust occurs when user-controlled input is used to make HTTP requests without adequate validation or network scoping. Axum itself does not introduce SSRF; it is the composition of an HTTP client, permissive routing parameters, and unchecked deserialization that creates the risk. For example, if an endpoint accepts a URL string or host + port fields and forwards requests using a crate such as reqwest or surf, an attacker can supply internal addresses, cloud metadata endpoints (e.g., 169.254.169.254), or SSRF-specific patterns that bypass intended allowlists.
In Rust, the type system and borrow checker reduce some classes of memory-safety bugs, but they do not prevent logical flaws like SSRF. A typical vulnerable handler might deserialize a JSON body into a struct with a url field and then call client.get(&input_url).send().await, without validating the host against an internal network allowlist or restricting outbound destinations to known public services. Axum extractors such as Json<T> and Query<T> make it easy to bind user input directly into request-building logic, which can lead to accidental exposure of internal services, container metadata, or cloud instance credentials when the client resolves private IPs or special-use hostnames.
Certain patterns common in Rust HTTP services amplify the surface: using a shared client with default timeouts, failing to strip sensitive headers before forwarding, and not enforcing a strict destination allowlist at the route level. Because Axum runs on Tokio, asynchronous handlers can inadvertently follow redirects or follow internal DNS resolutions that reach unexpected endpoints. The vulnerability is not in Rust or Axum per se, but in how developers wire network calls — for instance, missing port validation, overly permissive regex matchers for hostnames, and absent checks for IP loopback or link-local ranges. An SSRF scan performed by middleBrick can surface these logic flaws by probing endpoints that accept external URLs and checking whether the service reaches internal or restricted network targets, regardless of the language runtime.
Rust-Specific Remediation in Axum — concrete code fixes
Remediation centers on strict input validation, destination allowlisting, and safe handling of redirects and DNS resolution. Prefer using typed parameters instead of free-form URL strings, and validate hosts against a denylist/blocklist of private and reserved ranges before making outbound requests.
use axum::{routing::get, Json, Router};
use serde::{Deserialize, Serialize};
use std::net::IpAddr;
use reqwest::Client;
#[derive(Deserialize, Serialize)]
struct ProxyRequest {
host: String,
port: u16,
path: String,
}
/// Validate host against private/reserved ranges and loopback.
fn is_safe_destination(host: &str, ip: IpAddr) -> bool {
// Reject loopback, link-local, multicast, carrier-grade NAT
match ip {
IpAddr::V4(v4) if v4.is_loopback() -> false,
IpAddr::V4(v4) if v4.is_private() -> false,
IpAddr::V4(v4) if v4.is_link_local() -> false,
IpAddr::V4(v4) if v4.is_multicast() -> false,
IpAddr::V6(v6) if v6.is_loopback() -> false,
IpAddr::V6(v6) if v6.is_link_local() -> false,
IpAddr::V6(v6) if v6.is_multicast() -> false,
_ => true,
}
}
async fn proxy_handler(
Json(payload): Json<ProxyRequest>,
client: <axum::extract::State<AppState> as tower::Service<()>::Future
}
">client,
) -> Result<axum::Json<serde_json::Value>, (axum::http::StatusCode, String)> {
// Resolve before building request to enforce IP-based checks.
let resolved = tokio::net::lookup_host((payload.host.as_str(), payload.port))
.await
.map_err(|_| (axum::http::StatusCode::BAD_REQUEST, "Unable to resolve host"))?;
let ip = resolved
.map(|socket| socket.ip())
.next()
.ok_or_else(|| (axum::http::StatusCode::BAD_REQUEST, "No IP resolved"))?;
if !is_safe_destination(&payload.host, ip) {
return Err((axum::http::StatusCode::FORBIDDEN, "Destination not allowed".to_string()));
}
let url = format!("http://{}:{}{}", payload.host, payload.port, payload.path);
let response = client
.get(&url)
.timeout(std::time::Duration::from_secs(5))
.send()
.await
.map_err(|e| (axum::http::StatusCode::BAD_GATEWAY, e.to_string()))?;
let body = response
.text()
.await
.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok(Json(serde_json::from_str(&body).unwrap_or(serde_json::json!({ "raw": "**" }))))
}
#[tokio::main]
async fn main() {
let client = Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()
.expect("valid client");
let app = Router::new()
.route("/proxy", get(proxy_handler).post(proxy_handler))
.with_state(AppState { client });
axum::Server::bind(&("0.0.0.0:3000".parse().unwrap()))
.serve(app.into_make_service())
.await
.unwrap();
}
The above example demonstrates Rust-specific remediation in Axum: strict host resolution, IP-based allowlisting/denylisting, disabling redirects, and timeouts. By resolving to an IP address before constructing the request, you can reliably inspect and reject private, loopback, or reserved ranges. Using reqwest with a custom redirect policy prevents SSRF via open redirects or chained internal requests. middleBrick scans can validate that such controls are present by checking whether endpoints reject requests to internal targets like 127.0.0.1 or cloud metadata endpoints.
Additional practices include avoiding direct concatenation of user input into URLs, using typed query parameters where possible, and logging minimal request metadata without exposing sensitive headers. In production, combine these code-level controls with network segmentation and egress filtering so that even if SSRF occurs, the blast radius is limited. middleBrick’s per-category breakdowns can highlight missing validation and unsafe consumption patterns, providing prioritized findings and remediation guidance aligned with frameworks such as OWASP API Top 10.