Http Request Smuggling in Axum with Basic Auth
Http Request Smuggling in Axum with Basic Auth
Http Request Smuggling occurs when an API processes HTTP requests differently between a frontend proxy (like a load balancer or API gateway) and the application server, allowing attackers to smuggle requests across security boundaries. In Axum, this can manifest when requests that include Basic Auth credentials are handled inconsistently due to incomplete or ambiguous parsing, particularly around message body forwarding and header normalization.
Consider an Axum service that terminates TLS at a proxy and forwards requests internally over HTTP while preserving headers. If the proxy normalizes or removes hop-by-hop headers (e.g., Transfer-Encoding, Content-Length) differently than Axum expects, a request with Basic Auth can be split or duplicated. For example, an attacker might send a request with both Transfer-Encoding: chunked and a valid Authorization: Basic base64(credentials) header. If the proxy interprets this as two separate messages but Axum parses it as one, a second request without authentication can be smuggled through using the body of the first.
Basic Auth exacerbates the risk because the credentials move with the request. In Axum, handlers often retrieve the username and password via Extension<AuthExtractor> or middleware that reads the Authorization header. If the smuggling causes an authenticated request to be split, the second (smuggled) request might incorrectly inherit the credentials or bypass authentication checks entirely, leading to unauthorized access to user-specific data or administrative endpoints.
Real-world patterns include requests like:
POST /api/users HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: Basic dXNlcjpwYXNz
Transfer-Encoding: chunked
0
GET /api/admin/users HTTP/1.1
Host: example.com
If the proxy treats the 0 chunk as ending the first message and forwards the second request without the Authorization header, Axum may process the admin endpoint as unauthenticated. This violates the expectation that Basic Auth protects sensitive routes. The vulnerability is not in Axum’s routing or auth logic per se, but in how requests are framed and interpreted across layers.
Basic Auth-Specific Remediation in Axum
Remediation focuses on ensuring consistent request parsing and strict header handling. In Axum, use middleware to validate and normalize the Authorization header before routing, and avoid relying on implicit proxy behavior. Below are concrete, secure patterns.
1. Explicit Basic Auth Extraction Middleware
Create middleware that parses and validates Basic Auth on every request, rejecting malformed or ambiguous headers.
use axum::{
async_trait,
extract::Extension,
http::{Request, HeaderMap, header},
middleware::Next,
response::Response,
};
use std::convert::Infallible;
use base64::Engine;
pub struct BasicAuthMiddleware;
#[async_trait]
impl tower::Layer for BasicAuthMiddleware {
type Service = AuthMiddlewareService;
fn layer(&self, inner: S) -> Self::Service {
AuthMiddlewareService { inner }
}
}
pub struct AuthMiddlewareService {
inner: S,
}
#[async_trait]
impl tower::Service<Request<B>> for AuthMiddlewareService<S>
where
S: tower::Service<Request<B>, Response = Response, Error = Infallible> + Clone + Send + 'static,
S::Future: Send + 'static,
B: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = async_compression::futures::BoxFuture<'static, Result>;
fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request<B>) -> Self::Future {
let auth_header = req.headers().get(header::AUTHORIZATION);
match auth_header {
Some(value) => {
if let Ok(auth_str) = value.to_str() {
if auth_str.starts_with("Basic ") {
let encoded = auth_str.trim_start_matches("Basic ");
if let Ok(decoded) = base64::engine::general_purpose::STANDARD.decode(encoded) {
if let Ok(credentials) = String::from_utf8(decoded) {
let parts: Vec<&str> = credentials.splitn(2, ':').collect();
if parts.len() == 2 && !parts[0].is_empty() && !parts[1].is_empty() {
// Attach validated credentials to request extensions
req.extensions_mut().insert(parts);
} else {
// Reject malformed credentials early
let response = Response::builder()
.status(400)
.body(hyper::Body::from("Invalid Authorization header"))
.unwrap();
return async { Ok(response) }.boxed();
}
}
}
}
}
}
None => {
let response = Response::builder()
.status(401)
.header(header::WWW_AUTHENTICATE, "Basic realm=\"api\"")
.body(hyper::Body::from("Missing Authorization header"))
.unwrap();
return async { Ok(response) }.boxed();
}
}
self.inner.call(req).boxed()
}
}
// Usage in your router:
// let app = Router::new()
// .route("/admin/users", get(admin_handler))
// .layer(BasicAuthMiddleware);
2. Avoid Implicit Chunked Transfer Handling
Ensure your proxy and Axum agree on how request bodies are framed. Disable chunked transfer encoding at the proxy if possible, or enforce strict Content-Length validation in Axum before processing the body.
use axum::{routing::post, Router};
use std::net::SocketAddr;
async fn handler() -> &'static str {
"OK"
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/secure", post(handler))
// Add middleware that checks Content-Length header presence
.layer(BasicAuthMiddleware);
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
3. Proxy Configuration Consistency
Work with your infrastructure team to ensure the proxy normalizes headers identically to Axum. Specifically, ensure that Transfer-Encoding is not partially interpreted and that Content-Length is strictly respected before forwarding.