Header Injection in Axum with Mutual Tls
Header Injection in Axum with Mutual Tls
Header Injection in Axum with Mutual Tls occurs when an upstream service terminates mTLS and forwards requests into an Axum application over plain HTTP. In this setup, the client certificate is validated at the edge or load balancer, and the application assumes identity and authorization data are trustworthy. If the application then dynamically constructs response headers using values taken from the request (e.g., X-Forwarded-For, X-Forwarded-Proto, or custom headers) without strict validation, an attacker who can inject headers at the edge can propagate malicious header lines into the Axum response, leading to response splitting, cache poisoning, or cross-site scripting.
Mutual Tls changes the trust boundary but does not eliminate header injection. The TLS layer ensures the identity of the client to the endpoint, yet Axum code that reads headers from the incoming request and reuses them in outgoing responses remains vulnerable. For example, if Axum reads a header such as X-Custom-Location and uses it to set a Location or Refresh header, an attacker who controls an earlier hop can inject newline characters (e.g., \n or \r\n) to append additional headers or switch to a malicious body. Axum’s extractor patterns, such as using HeaderMap or typed extractors, will happily surface injected headers unless you explicitly validate or sanitize them.
Consider a scenario where Axum builds a redirect using a request-supplied path. If the edge terminates mTLS and forwards with headers that include X-Redirect-Url, and Axum directly uses that header to form a Location header, an attacker who can influence that upstream header can inject a second Location line or a Set-Cookie line. This is a classic HTTP response splitting pattern, and it is not prevented by transport encryption or client certificate validation. The presence of Mutual Tls may even give a false sense of security, leading developers to skip input validation on headers that appear after the mTLS boundary.
To observe this in practice, you can send requests through a proxy that terminates mTLS and injects headers before they reach Axum. A crafted header such as X-Location: https://evil.example.com\r\nSet-Cookie: session=stolen can result in a response that redirects and sets a cookie if Axum does not sanitize the value. middleBrick can detect such risks by scanning the unauthenticated attack surface and checking whether response headers are constructed from unchecked inputs, even when Mutual Tls is in use. The scanner’s checks include Authentication, Input Validation, and Data Exposure, which help highlight where user-influenced headers reach response construction logic.
Mutual Tls-Specific Remediation in Axum
Remediation focuses on never trusting headers that originate after the mTLS boundary. In Axum, treat all incoming headers as potentially hostile, even when the transport is protected by client certificates. Validate, normalize, and explicitly allowlist any headers that influence response construction. Avoid concatenating or interpolating raw header values into Location, Set-Cookie, or other sensitive headers.
Use strict header extraction and reject unexpected or malformed values. Prefer typed extractors with strong validation, and do not fall back to raw string concatenation for security-sensitive headers. When you must forward or reflect a header value, canonicalize it (e.g., trim whitespace, reject newlines, enforce URI schemes for redirects) and encode as appropriate for the target header.
Below are concrete Axum examples demonstrating secure handling when Mutual Tls is used at the edge.
use axum::{routing::get, Router, extract::headers::authorization::Bearer, http::HeaderMap, response::IntoResponse};
use std::net::SocketAddr;
// Safe header usage: explicit allowlist and validation
async fn build_redirect(uri: String) -> impl IntoResponse {
// Validate that the URI is safe: non-empty, allowed host, no newlines
if uri.contains('\n') || uri.contains('\r') {
return (axum::http::StatusCode::BAD_REQUEST, "Invalid URI");
}
// Use a controlled base to avoid open redirects
let base = "https://app.example.com";
format!("{}{}", base, uri).into_response()
}
async fn handle_request(headers: HeaderMap) -> impl IntoResponse {
// Explicitly extract and validate a trusted header
let x_request_id = headers.get("X-Request-ID")
.and_then(|v| v.to_str().ok())
.map(|s| s.trim())
.filter(|s| !s.is_empty());
match x_request_id {
Some(id) => format!("Request ID: {}", id).into_response(),
None => (axum::http::StatusCode::BAD_REQUEST, "Missing X-Request-ID").into_response(),
}
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/redirect", get(|uri: String| async move { build_redirect(uri).await }))
.route("/id", get(|headers: HeaderMap| async move { handle_request(headers).await }));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
In environments where Mutual Tls is handled by a proxy or load balancer, configure Axum to read the verified client identity from a trusted header (e.g., a certificate-derived fingerprint) rather than constructing security decisions from mutable headers. Combine this with a deny-by-default policy for response headers and continuous scanning using tools like middleBrick to ensure header injection risks are caught before deployment.