
Preventing HTTP Request Smuggling in Rust: Building Safer Proxy and Gateway Services
What HTTP request smuggling looks like
Request smuggling usually appears when one component interprets a request as ending at a different point than another component does. The classic causes are:
- Conflicting
Content-LengthandTransfer-Encodingheaders - Duplicate or malformed headers
- Differences in how HTTP/1.1 parsers treat whitespace, line endings, or chunked encoding
- Proxy chains where each hop has slightly different parsing rules
A typical attack sends a crafted request that one server sees as a single request, while another sees as two requests. The attacker then “smuggles” a hidden request past the front end.
Why Rust developers should care
You are most exposed when you:
- Build a reverse proxy or gateway in Rust
- Accept raw HTTP and forward it to another service
- Normalize or rewrite headers
- Implement custom tunneling, connection pooling, or request batching
- Mix HTTP/1.1 and HTTP/2 in the same edge layer
If your Rust code only serves as a normal application behind a well-configured web server, the risk is lower. But if your code participates in request forwarding, you must treat parsing and forwarding as security-sensitive logic.
The core defense: reject ambiguity
The safest strategy is simple: never forward ambiguous requests.
Your Rust service should reject requests that contain:
- Both
Content-LengthandTransfer-Encoding - Multiple
Content-Lengthheaders - Invalid chunked encoding
- Obsolete line folding or malformed header syntax
- Unexpected control characters in header names or values
If a request is ambiguous, do not try to “guess” the correct interpretation. Ambiguity is the vulnerability.
Recommended policy
| Condition | Safe action |
|---|---|
Content-Length and Transfer-Encoding both present | Reject with 400 Bad Request |
Multiple Content-Length headers | Reject unless your stack explicitly canonicalizes them and you verify consistency |
| Invalid chunk framing | Reject immediately |
| Header names with invalid bytes | Reject |
| Duplicate hop-by-hop headers | Strip or reject depending on role |
| Unknown transfer semantics | Reject |
Use a modern HTTP stack, but know its limits
In Rust, most production HTTP services use hyper, often through frameworks like axum, warp, or actix-web. These libraries are generally safer than ad hoc parsers, but they do not eliminate protocol risk if you manually manipulate raw requests or bridge between protocols.
Good practices with Rust HTTP libraries
- Prefer framework-managed request parsing over raw socket handling
- Avoid reconstructing HTTP requests manually from strings
- Do not forward user-supplied headers blindly
- Use well-tested client libraries for outbound requests
- Keep front-end and back-end components aligned on HTTP version and parsing rules
If you are writing a proxy, choose a library that enforces strict parsing and exposes enough metadata to validate requests before forwarding.
Example: rejecting ambiguous requests in a Rust gateway
The following example shows a small axum-based gateway that rejects requests with conflicting body framing before forwarding them.
use axum::{
body::Body,
extract::Request,
http::{header, HeaderMap, StatusCode},
middleware::Next,
response::Response,
};
fn has_conflicting_framing(headers: &HeaderMap) -> bool {
let has_content_length = headers.get_all(header::CONTENT_LENGTH).iter().count() > 0;
let has_transfer_encoding = headers.get(header::TRANSFER_ENCODING).is_some();
has_content_length && has_transfer_encoding
}
fn has_multiple_content_length(headers: &HeaderMap) -> bool {
headers.get_all(header::CONTENT_LENGTH).iter().count() > 1
}
pub async fn reject_smuggled_requests(req: Request, next: Next) -> Response {
let headers = req.headers();
if has_conflicting_framing(headers) || has_multiple_content_length(headers) {
return (StatusCode::BAD_REQUEST, "ambiguous request framing").into_response();
}
// Optional: reject malformed or unexpected transfer encodings.
if let Some(te) = headers.get(header::TRANSFER_ENCODING) {
if te != "chunked" {
return (StatusCode::BAD_REQUEST, "unsupported transfer encoding").into_response();
}
}
next.run(req).await
}This is not a complete smuggling defense, but it demonstrates the right posture: validate framing early, reject ambiguity, and keep the policy explicit.
Important caveat
If your proxy terminates HTTP/1.1 and forwards to another service, you must validate both the incoming request and the outgoing request construction. A request can become dangerous not only when received, but also when reserialized.
Normalize headers carefully
Header normalization is a common source of smuggling bugs. Two servers may disagree on whether headers are duplicated, case-insensitive, folded, or equivalent after trimming whitespace.
Safe header handling rules
- Treat header names as case-insensitive, but preserve canonical forms internally
- Reject control characters in header values
- Strip hop-by-hop headers before forwarding
- Never forward
Connection-listed headers downstream - Do not preserve duplicate framing headers
- Avoid concatenating header values unless the semantics are well-defined
Hop-by-hop headers to strip
When acting as a proxy, remove headers that apply only to the current connection:
ConnectionKeep-AliveProxy-AuthenticateProxy-AuthorizationTEexcept fortrailersTrailerTransfer-EncodingUpgrade
This is especially important if you accept a request from one client and forward it to another server. Forwarding connection-specific headers can create parsing confusion or leak transport-level intent.
Prefer HTTP/2 or HTTP/3 at the edge when possible
Request smuggling is primarily an HTTP/1.1 parsing problem. HTTP/2 and HTTP/3 use binary framing, which removes many of the classic boundary ambiguities.
That said, the risk does not disappear entirely:
- You may still terminate HTTP/2 and forward HTTP/1.1 internally
- Some libraries downgrade requests automatically
- Mixed-protocol deployments can reintroduce inconsistencies
Practical guidance
- Use HTTP/2 or HTTP/3 for client-facing connections when your stack supports it
- Ensure the downgrade path to HTTP/1.1 is strict and well-tested
- Keep proxy and origin servers aligned on body framing rules
- Disable protocol features you do not need, especially in custom gateways
If your service is a pure HTTP/2 endpoint and does not translate requests, your exposure is much lower than a custom HTTP/1.1 proxy.
Validate request bodies before forwarding
A smuggling payload often relies on body framing. If your gateway reads a request body and then forwards it, you must ensure the body length is unambiguous and fully consumed.
Best practices
- Read the body using the framework’s safe abstractions
- Enforce maximum body sizes
- Reject requests with missing or invalid length information when required
- Do not stream partially parsed bodies into another parser
- Ensure the entire body is consumed or the connection is closed
If you proxy requests over a keep-alive connection, a partially consumed body can desynchronize the next request on that connection. That is one of the most dangerous failure modes.
Design your proxy as a strict translator, not a pass-through
A secure Rust proxy should behave like a policy-enforcing translator. It should not simply copy bytes from one socket to another.
A safer forwarding model
- Parse the incoming request with a trusted HTTP library
- Validate method, path, headers, and body framing
- Remove hop-by-hop headers
- Rebuild a clean outbound request
- Send it using a client library with explicit framing rules
This model reduces the chance that malformed input survives intact into the backend.
Example forwarding checklist
- Method is allowed
- URI is normalized and within policy
- Host header matches expected routing rules
- No conflicting framing headers
- Body size is within limit
- Only approved headers are forwarded
- Connection reuse is safe for the request type
Test for smuggling conditions explicitly
Security testing for request smuggling should be part of your integration test suite. Do not rely only on unit tests for header parsing.
Useful test cases
Content-Length: 10plusTransfer-Encoding: chunked- Two different
Content-Lengthheaders - Chunked body with invalid chunk size
- Header values containing CR or LF
- Requests with extra bytes after the declared body
- Pipelined requests on the same connection
- Proxy-to-backend downgrade from HTTP/2 to HTTP/1.1
Example test strategy
Use a raw TCP client in tests to send malformed requests directly to your gateway. Then verify that:
- The request is rejected
- The connection is closed when appropriate
- No partial request reaches the backend
- No second request is interpreted from leftover bytes
This kind of testing is especially important if you maintain custom middleware, reverse proxy logic, or connection pooling.
Operational hardening matters too
Even a well-written Rust service can be undermined by inconsistent infrastructure.
Deployment recommendations
- Keep front-end and back-end servers updated
- Avoid mixing parsers from different vendors without testing
- Disable request pipelining unless you need it
- Set conservative body size limits
- Use separate trust boundaries for public and internal traffic
- Log and alert on rejected framing anomalies
If your edge proxy rejects malformed requests but your origin server accepts them, an attacker may still find a path through another component. Consistency across the stack is essential.
Common mistakes to avoid
| Mistake | Why it is risky |
|---|---|
| Forwarding raw headers unchanged | Preserves ambiguous or hop-by-hop semantics |
Accepting both Content-Length and Transfer-Encoding | Enables classic smuggling payloads |
| Reusing connections after partial reads | Can desynchronize request boundaries |
| Trusting upstream proxies blindly | Different parsers may disagree |
| Normalizing headers by string concatenation | Can merge values in unsafe ways |
| Ignoring malformed requests in logs only | Attack traffic should be rejected, not merely observed |
A secure mindset for Rust gateway code
Rust helps you write robust systems, but request smuggling is about protocol correctness, not memory safety. The key is to make your gateway strict, boring, and explicit:
- Parse with a trusted library
- Reject ambiguous framing
- Strip connection-specific headers
- Rebuild outbound requests carefully
- Test malformed inputs aggressively
- Keep all hops in the chain aligned
If your Rust service sits on the boundary between clients and internal services, treat HTTP parsing as part of your security perimeter. That is where request smuggling defenses belong.
