Ssrf in Axum with Mutual Tls
Ssrf in Axum with Mutual Tls — how this specific combination creates or exposes the vulnerability
Server-Side Request Forgery (SSRF) in an Axum service that uses Mutual TLS (mTLS) arises when the server acts as an HTTP client and makes outbound requests to user-supplied URLs while mTLS is enforced on inbound connections only. mTLS protects the server from unauthenticated clients by requiring client certificates, but it does not automatically protect outbound requests. If the application forwards attacker-controlled URLs to an HTTP client without network-level restrictions, the server can be tricked into initiating requests to internal services that are only reachable because the server itself holds a valid client certificate and trust material.
In Axum, this typically occurs when a route handler deserializes a URL from a request and passes it to a reqwest or hyper client. For example, an endpoint that accepts a target parameter to proxy or fetch data may resolve internal hostnames like http://metadata.local or http://169.169.169.254 (cloud metadata) because the server’s outbound mTLS credentials are trusted by those internal endpoints. Outbound mTLS is uncommon; most mTLS setups terminate at the service perimeter. Therefore, SSRF emerges from the mismatch: inbound authentication via mTLS does not equate to outbound authorization, and the server may implicitly trust its own requests.
Attack patterns enabled by this combination include metadata service exfiltration, internal service enumeration via private IPs, and protocol smuggling through trusted channels. Because the server presents a valid client certificate to internal endpoints, requests succeed where external clients would fail. MiddleBrick’s checks for SSRF and authentication map to OWASP API Top 10 A01:2023 and API Security Top 10, identifying risky trust boundaries where outbound paths are not explicitly constrained.
Mutual Tls-Specific Remediation in Axum — concrete code fixes
Remediation focuses on strict input validation, network segregation, and explicit outbound policies rather than relying on mTLS for request authorization. Below are concrete Axum examples using reqwest with a connector that limits egress.
1. Example: Axum handler with validated URL and restricted client
use axum::{routing::get, Router, Json};
use serde::Deserialize;
use reqwest::Url;
use std::net::IpAddr;
#[derive(Deserialize)]
struct FetchRequest {
target: String,
}
async fn fetch_proxy(Json(payload): Json) -> Result {
// Validate and parse URL
let url = Url::parse(&payload.target).map_err(|_| (axum::http::StatusCode::BAD_REQUEST, "Invalid URL".into()))?;
// Allowlist hostnames/IPs
let allowed_hosts = ["api.example.com", "10.0.0.1"];
let host = url.host_str().ok_or((axum::http::StatusCode::BAD_REQUEST, "Missing host".into()))?;
if !allowed_hosts.contains(&host) {
return Err((axum::http::StatusCode::FORBIDDEN, "Host not allowed".into()));
}
// Block private IP ranges at the URL level
if let Some(ip) = url.ip_host() {
if ip.is_private() {
return Err((axum::http::StatusCode::FORBIDDEN, "Private IP not allowed".into()));
}
}
// Build a restricted client (no proxy, timeouts)
let client = reqwest::Client::builder()
.disable_redirects()
.timeout(std::time::Duration::from_secs(5))
.build()
.map_err(|_| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, "Client build error".into()))?;
let resp = client.get(url).send().await.map_err(|e| (axum::http::StatusCode::BAD_GATEWAY, e.to_string()))?;
let text = resp.text().await.map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok(text)
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/fetch", get(fetch_proxy));
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
The example enforces hostname allowlisting and blocks private IPs before constructing the request. This compensates for the absence of outbound mTLS controls. Note that mTLS certificates for the server remain necessary for inbound client authentication but are not reused for outbound calls.
2. Example: Outbound network policy via middleware or connector
use reqwest::Client;
use std::sync::Arc;
struct SafeConnector {
inner: reqwest::tls::TlsConnector,
allowed_subnets: Vec,
}
impl SafeConnector {
fn new(allowed_subnets: Vec) -> Self {
let tls = reqwest::tls::TlsConnector::new();
Self { inner: tls, allowed_subnets }
}
}
// In practice, integrate with reqwest::ConnectorBuilder to enforce egress rules.
// This snippet illustrates the concept; full implementation requires custom connector logic.
For production, consider deploying egress controls at the platform level (network policies, service mesh) so that even misconfigured clients cannot reach unintended endpoints. Combine this with runtime scanning (such as MiddleBrick) to detect SSRF and authentication misconfigurations across your API surface.
Related CWEs: ssrf
| CWE ID | Name | Severity |
|---|---|---|
| CWE-918 | Server-Side Request Forgery (SSRF) | CRITICAL |
| CWE-441 | Unintended Proxy or Intermediary (Confused Deputy) | HIGH |