CRITICAL http request smugglingaxum

Http Request Smuggling in Axum

How HTTP Request Smuggling Manifests in Axum

HTTP Request Smuggling (HRS) exploits discrepancies in how servers parse HTTP request boundaries, typically via conflicting Content-Length and Transfer-Encoding headers. In Axum applications—which use the hyper HTTP library under the hood—this vulnerability arises when the frontend proxy (e.g., Nginx, Cloudflare) and the Axum backend interpret these headers differently. Axum's default hyper server strictly follows RFC 7230, rejecting requests with both headers present. However, misconfigured reverse proxies or custom middleware that modifies request bodies can introduce ambiguity.

Two primary attack patterns target Axum apps:

  • CL.TE (Content-Length / Transfer-Encoding): The frontend uses Content-Length to determine request length, while Axum (via hyper) prioritizes Transfer-Encoding: chunked. An attacker sends a request with both headers, causing the frontend to forward only part of the body to Axum, leaving residual bytes that get prepended to the next request.
  • TE.CL (Transfer-Encoding / Content-Length): The frontend respects Transfer-Encoding, but Axum erroneously processes Content-Length (possible if custom body aggregation logic overrides hyper's parsing). This allows injecting a second request within the chunked body.

Consider an Axum handler that manually reads the request body without validating headers:

use axum::body::Bytes;
use axum::http::Request;
use axum::response::Response;
use tower::ServiceExt;

async fn vulnerable_handler(
   mut request: Request<axum::body::Body>
) -> Result<Response, (axum::http::StatusCode, String)> {
    // Manually aggregate body without checking header conflicts
    let whole_body = Bytes::from(request.body_mut().take().await.map_err(|_| 
        (axum::http::StatusCode::BAD_REQUEST, "Stream error".into())
    )?);
    
    // Process body...
    Ok(Response::new("OK".into()))
}

Here, request.body_mut().take().await aggregates the entire stream, but if a proxy forwards a request with both headers and stops reading at Content-Length, leftover bytes remain in the connection. Axum's subsequent request will read those bytes as its start line, leading to request smuggling.

Axum-Specific Detection

Detecting HRS in Axum requires testing how the application handles ambiguous Content-Length and Transfer-Encoding headers. middleBrick's Input Validation and Data Exposure checks actively probe for these discrepancies by sending crafted requests with conflicting headers and analyzing responses for evidence of request boundary confusion.

For example, middleBrick might send a request like:

POST /api/v1/submit HTTP/1.1
Host: target.com
Content-Length: 4
Transfer-Encoding: chunked

0

GET /admin HTTP/1.1
Host: target.com

If the frontend proxy respects Content-Length: 4 (reading only 0 ) but Axum processes the chunked body, the GET /admin becomes a second request. middleBrick detects this by:

  • Observing unexpected 200 OK responses to the second request (indicating it reached the backend).
  • Checking for 400 Bad Request errors that reveal proxy vs. backend parsing differences.
  • Analyzing response timing anomalies (smuggled requests may cause delays).

To scan an Axum API with middleBrick:

middlebrick scan https://api.youraxumapp.com

The report will flag HRS under the Input Validation category, showing severity (typically Critical), and provide the exact request pattern that triggered the vulnerability. middleBrick's OpenAPI spec analysis also cross-references endpoints that accept POST<code>PUT</code> with body parameters, prioritizing them for HRS testing.

Axum-Specific Remediation

Fix HRS in Axum by ensuring consistent header interpretation between all infrastructure layers. The primary defense is to reject any request containing both Content-Length and Transfer-Encoding headers, as per RFC 7230 Section 3.3.3. Axum's middleware system allows you to enforce this at the application edge.

Implement a validation middleware early in your tower stack:

use axum::{
    body::Body,
    http::{Request, StatusCode},
    response::Response,
};
use tower::{Layer, Service};
use std::task::{Context, Poll};

#[derive(Clone)]
struct RejectConflictingHeadersLayer;

impl<S> Layer<S> for RejectConflictingHeadersLayer {
    type Service = RejectConflictingHeadersService<S>;

    fn layer(&self, service: S) -> Self::Service {
        RejectConflictingHeadersService { inner: service }
    }
}

struct RejectConflictingHeadersService<S> {
    inner: S,
}

impl<S> Service<Request<Body>> for RejectConflictingHeadersService<S>
where
    S: Service<Request<Body>, Response = Response> + Clone + Send + 'static,
    S::Future: Send,
{
    type Response = S::Response;
    type Error = S::Error;
    type Future = std::pin::Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;

    fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        self.inner.poll_ready(cx)
    }

    fn call(&self, mut request: Request<Body>) -> Self::Future {
        let headers = request.headers();
        let has_content_length = headers.contains_key("content-length");
        let has_transfer_encoding = headers.contains_key("transfer-encoding");

        if has_content_length && has_transfer_encoding {
            // Immediately reject ambiguous requests
            let response = Response::builder()
                .status(StatusCode::BAD_REQUEST)
                .body(Body::from("Conflicting Content-Length and Transfer-Encoding headers"))
                .unwrap();
            return Box::pin(std::future::ready(Ok(response)));
        }

        let future = self.inner.call(request);
        Box::pin(async move { future.await })
    }
}

// Usage in your Axum router
use axum::Router;

let app = Router::new()
    .route("/api", post(vulnerable_handler))
    .layer(RejectConflictingHeadersLayer);

Additionally, ensure your reverse proxy (Nginx, Envoy, etc.) is configured to strip or reject conflicting headers before they reach Axum. For Nginx:

location / {
    proxy_set_header Transfer-Encoding "";
    proxy_pass http://axum_backend;
}

middleBrick's remediation guidance in the scan report will include these Axum-specific code patterns and proxy configurations, mapping them to OWASP API Top 10: API5:2023 — Broken Function Level Authorization (where HRS often leads to privilege escalation).

Frequently Asked Questions

Why is Axum vulnerable to HTTP Request Smuggling by default?
Axum's underlying hyper library correctly rejects requests with both Content-Length and Transfer-Encoding headers. However, vulnerability emerges when a frontend proxy (like Nginx) and Axum interpret these headers differently—for example, if the proxy uses Content-Length while Axum uses Transfer-Encoding. Custom middleware that manually reads request bodies without header validation can also introduce discrepancies.
How does middleBrick detect HTTP Request Smuggling in Axum APIs?
middleBrick's Input Validation check sends crafted requests with conflicting Content-Length and Transfer-Encoding headers to your Axum endpoint. It then analyzes responses for signs of request boundary confusion—such as unexpected 200 OK responses to smuggled secondary requests or timing anomalies. The scan report identifies the vulnerable endpoint and provides the exact request pattern that triggered the issue.