Dns Rebinding in Axum (Rust)
Dns Rebinding in Axum with Rust — how this specific combination creates or exposes the vulnerability
DNS rebinding is a client-side attack where a malicious webpage causes a victim’s browser to send requests to an internal or private IP address that the victim can reach but external hosts cannot. In Axum applications written in Rust, this typically arises when server-side HTTP clients (for example code that fetches a user-supplied URL) do not enforce strict destination validation. Even though Rust’s type system and ownership model reduce classes of memory-safety bugs, they do not prevent logical weaknesses such as trusting a hostname without verifying its resolved IP against an allowlist. An attacker can serve a page with JavaScript that repeatedly changes DNS records to point a benign-looking domain to 127.0.0.1 or to internal infrastructure. When the victim’s browser makes requests, Axum-based backend code that forwards requests or performs service discovery may follow the malicious redirects and expose internal endpoints to the browser, bypassing same-origin policy.
Consider an Axum handler that accepts a URL parameter and performs an outbound HTTP request using reqwest and hyper. If the handler resolves the hostname once and caches the IP, or does not validate that the resolved IP belongs to an allowed range, an attacker can manipulate DNS so that the first resolution is external (passing basic hostname checks) and a subsequent rebind redirects the request to a sensitive internal service. Because Axum is often used for building APIs that integrate with internal services (databases, admin interfaces, or legacy HTTP endpoints), the presence of such a handler increases exposure. Rust’s async runtime and connection pooling can amplify the risk: a single compromised handler may be reused across requests, and the application may trust the absence of errors as an indicator of safety, while the real issue is insufficient network boundary enforcement. Common triggers include permissive CORS configurations, open HTTP proxy behavior, or handlers that perform health checks or service discovery based on user input.
To detect this pattern with middleBrick, you can submit your Axum service URL for a scan. The tool runs 12 parallel security checks, including Input Validation and SSRF, and tests whether the API allows requests to internal or private IPs via controlled rebinding indicators. Because middleBrick performs unauthenticated, black-box testing and supports OpenAPI/Swagger spec analysis with full $ref resolution, it can correlate runtime behavior with your spec to highlight routes that accept user-controlled URLs without adequate network boundary checks. The findings include severity, remediation guidance, and mapping to frameworks such as OWASP API Top 10, helping you prioritize fixes without needing to understand the scanner’s internals.
Rust-Specific Remediation in Axum — concrete code fixes
Mitigating DNS rebinding in Axum applications centers on validating destinations before use, avoiding blind trust in hostnames, and enforcing network boundaries at the handler and client layers. In Rust, you can leverage type safety and crates like url, hyper, and ipnet to implement strict checks. The key practices are: (1) reject user-controlled URLs that point to private or loopback IPs after resolution, (2) enforce an explicit allowlist of hostnames or IP ranges for outbound requests, and (3) avoid caching resolved IPs across requests if the original hostname is mutable. Below are concrete Axum handler examples demonstrating secure patterns.
First, define a helper to validate that a given URL resolves to an allowed destination. This example uses trust-dns-resolver for synchronous resolution and ipnet to check against private ranges. In production, you may choose an async resolver to match your runtime; the principle remains the same: resolve, then verify.
use url::Url;
use ipnet::{IpNet, Ipv4Net};
use std::net::IpAddr;
use trust_dns_resolver::Resolver;
use trust_dns_resolver::config::{ResolverConfig, ResolverOpts};
fn is_private_or_loopback(ip: IpAddr) -> bool {
let private_networks: Vec<IpNet> = vec![
"0.0.0.0/8".parse().unwrap(),
"10.0.0.0/8".parse().unwrap(),
"100.64.0.0/10".parse().unwrap(),
"127.0.0.0/8".parse().unwrap(),
"169.254.0.0/16".parse().unwrap(),
"172.16.0.0/12".parse().unwrap(),
"192.0.0.0/24".parse().unwrap(),
"192.168.0.0/16".parse().unwrap(),
"224.0.0.0/4".parse().unwrap(),
"240.0.0.0/4".parse().unwrap(),
"255.255.255.255/32".parse().unwrap(),
];
private_networks.iter().any(|net| net.contains(ip))
}
fn validate_url(raw: &str) -> Result<Url, String> {
let parsed = Url::parse(raw).map_err(|e| format!("Invalid URL: {}", e))?;
let host = parsed.host_str().ok_or_else(|| "Missing host".to_string())?;
let resolver = Resolver::new(ResolverConfig::default(), ResolverOpts::default())
.map_err(|e| format!("Resolver error: {}", e))?;
let addrs = resolver.lookup_ip(host).map_err(|e| format!("DNS lookup failed: {}", e))?;
for addr in addrs.iter() {
if is_private_or_loopback(addr.into()) {
return Err(format!("Blocked private/loopback IP: {}", addr));
}
}
Ok(parsed)
}
Second, integrate this validation into an Axum handler that forwards requests or performs service discovery. Ensure you construct the client with appropriate timeouts and do not reuse connections in a way that bypasses validation. The example below shows an extractor that validates before proceeding, returning a 400 if the destination is unacceptable.
use axum::{
routing::get, Router, extract::Query, http::StatusCode, response::IntoResponse
};
use serde::Deserialize;
#[derive(Deserialize)]
struct FetchParams {
target: String,
}
async fn fetch_handler(Query(params): Query<FetchParams>) -> impl IntoResponse {
match validate_url(¶ms.target) {
Ok(url) => {
// Proceed with a safe, validated URL using a client configured with timeouts
// let client = reqwest::Client::builder()
// .timeout(Duration::from_secs(5))
// .build()
// .unwrap();
// let resp = client.get(url).send().await;
(StatusCode::OK, format!("Validated target: {}", url))
}
Err(msg) => (StatusCode::BAD_REQUEST, msg),
}
}
fn app() -> Router {
Router::new().route("/fetch", get(fetch_handler))
}
Finally, for applications that do not need to fetch arbitrary URLs, avoid user-controlled destinations entirely. Use fixed endpoints and path parameters that map to known services, and apply route-level CORS policies to restrict origins. middleBrick’s Pro plan supports continuous monitoring and CI/CD integration, so you can automatically fail builds if changes introduce permissive URL handling. Its scans include Input Validation and SSRF checks, providing prioritized findings with remediation guidance mapped to frameworks like OWASP API Top 10, which helps teams address DNS rebinding and related classes of vulnerabilities efficiently.