The Problem
When malformed or abnormal HTTP requests are interpreted by one or more entities in the data flow between the user and the web server, such as a proxy or firewall, they can be interpreted inconsistently, allowing the attacker to "smuggle" a request to one device without the other device being aware of it.
HTTP Request Smuggling
HTTP Request Smuggling exploits discrepancies in how front-end (proxy/load balancer/WAF) and back-end servers parse HTTP requests, specifically around where one request ends and the next begins.
How It Works
HTTP/1.1 uses two headers to indicate body length:
-
Content-Length(CL): specifies body size in bytes -
Transfer-Encoding(TE): uses chunked encoding
When a request contains both headers (or malformed variants), different servers may prioritize differently:
| Variant | Front-end uses | Back-end uses | Result |
|---|---|---|---|
| CL.TE | Content-Length | Transfer-Encoding | Front-end forwards extra data that back-end treats as a new request |
| TE.CL | Transfer-Encoding | Content-Length | Back-end stops reading early; leftover bytes become the next request |
| TE.TE | Transfer-Encoding | Transfer-Encoding | One server is tricked by an obfuscated TE header (e.g., Transfer-Encoding: chunked\r\n Transfer-Encoding: x) |
Example (CL.TE)
| Variant | Front-end uses | Back-end uses | Result |
|---|---|---|---|
| CL.TE | Content-Length | Transfer-Encoding | Front-end forwards extra data that back-end treats as a new request |
| TE.CL | Transfer-Encoding | Content-Length | Back-end stops reading early; leftover bytes become the next request |
| TE.TE | Transfer-Encoding | Transfer-Encoding | One server is tricked by an obfuscated TE header (e.g., Transfer-Encoding: chunked\r\n Transfer-Encoding: x) |
Example (CL.TE)
POST / HTTP/1.1
Host: example.com
Content-Length: 13
Transfer-Encoding: chunked
0\r\n
\r\n
SMUGGLED
The front-end reads 13 bytes (the full body). The back-end processes chunked encoding, sees 0 (end of chunks), and treats SMUGGLED as the start of the next request — which could be crafted to bypass access controls, poison caches, or hijack other users' requests.
Impact
- Bypass security controls (WAF, authentication)
- Cache poisoning — serve malicious content to other users
- Session hijacking — prepend attacker-controlled headers to another user's request
- Credential theft via request redirection
Mitigations & Solutions
Infrastructure level:
- Use HTTP/2 end-to-end — HTTP/2 has a binary framing layer that eliminates ambiguity in request boundaries
- Normalize requests at the front-end — ensure the proxy resolves ambiguous requests before forwarding
- Disable connection reuse between front-end and back-end (performance trade-off but eliminates the attack surface)
- Use the same web server software on all layers to ensure consistent parsing
Configuration level:
5. Reject ambiguous requests — configure proxies/servers to return 400 Bad Request when both Content-Length and Transfer-Encoding are present
6. Disable Transfer-Encoding: chunked if not needed
7. Configure WAF rules to detect and block requests with conflicting length indicators
Application level:
8. Validate incoming requests — reject requests with duplicate or conflicting Content-Length/Transfer-Encoding headers
9. Use strict HTTP parsing in your web framework (e.g., Gunicorn's --strip-header-spaces, Nginx's ignore_invalid_headers off)
10. Set timeouts on back-end connections to limit the window for smuggled requests to persist
Detection:
11. Monitor for anomalies — unusual 400/405 errors, mismatched access logs between front-end and back-end
12. Use tools like Burp Suite's HTTP Request Smuggler extension for testing
The most effective fix is upgrading to HTTP/2 between all hops and rejecting ambiguous requests at the edge.
Why Both Headers = Smuggling Risk
The HTTP/1.1 spec (RFC 7230) says: if both Transfer-Encoding and Content-Length are present, Transfer-Encoding must take priority and Content-Length must be ignored. The problem is not all servers follow this rule the same way.
The Core Issue: Disagreement on Request Boundaries
When a proxy sits in front of a backend server, both need to agree on where each request starts and ends. These two headers are the only way to determine that:
-
Content-Length: 50→ "the body is exactly 50 bytes" -
Transfer-Encoding: chunked→ "the body is split into chunks, ending with a0\r\nchunk"
When both are present, one server might use Content-Length and the other might use Transfer-Encoding — so they see different request boundaries.
Concrete Example
POST / HTTP/1.1
Host: example.com
Content-Length: 6
Transfer-Encoding: chunked
0\r\n
\r\n
X
What the proxy sees (if it trusts Content-Length: 6):
Body = "0\r\n\r\nX" (6 bytes — one complete request, forwards everything)
What the backend sees (if it trusts Transfer-Encoding: chunked):
Chunk: "0" → end of chunks → request is done
Leftover: "X" → this must be the START of a NEW request!
That X is now smuggled. The attacker controls it, and it gets prepended to the next legitimate user's request. In practice, X would be a full malicious request like:
GET /admin HTTP/1.1
Host: example.com
Why This is Dangerous
| Attack | How |
|---|---|
| Bypass WAF/auth | The proxy checks the outer request (looks safe), but the backend executes the hidden inner request |
| Session hijacking | The smuggled fragment merges with the next user's request, so the attacker's headers (e.g., a redirect) get applied to that user |
| Cache poisoning | The smuggled request causes the cache to store attacker-controlled content under a legitimate URL |
Why Rejecting Both Headers Fixes It
If neither server ever processes a request containing both headers (returns 400 immediately), there's no ambiguity to exploit — both sides always agree on how to parse the body length. That's exactly what the Nginx rule we added does:
# If Transfer-Encoding AND Content-Length are both present → reject
if ($smuggle_check = "TECL") {
return 400;
}
No ambiguity = no smuggling.
Testing HTTP Request Smuggling Protection
Validate Nginx Config Syntax
sudo nginx -t
Should output: syntax is ok and test is successful. Then reload:
sudo systemctl reload nginx
Test That Ambiguous Requests Are Rejected (400)
Send a request with both headers using curl:
# This should return HTTP 400
curl -i -X POST https://your-domain.com/ \
-H "Transfer-Encoding: chunked" \
-H "Content-Length: 6" \
-d "0\r\n\r\nX"
Expected: HTTP/1.1 400 Bad Request
Test with a normal request (should still work):
# Content-Length only — should return 200/normal response
curl -i -X POST https://your-domain.com/api/some-endpoint \
-H "Content-Type: application/json" \
-d '{"key": "value"}
## Transfer-Encoding only — should also work
curl -i -X POST https://your-domain.com/api/some-endpoint \
-H "Transfer-Encoding: chunked" \
-H "Content-Type: application/json" \
-d '{"key": "value"}'
Expected: Normal responses (not 400).
Test with Python Script
import socket
import ssl
host = "your-domain.com"
port = 443
## Craft an ambiguous request with both headers
smuggle_request = (
"POST / HTTP/1.1\r\n"
"Host: {}\r\n"
"Content-Length: 6\r\n"
"Transfer-Encoding: chunked\r\n"
"\r\n"
"0\r\n"
"\r\n"
"X"
).format(host)
ctx = ssl.create_default_context()
sock = socket.create_connection((host, port))
sock = ctx.wrap_socket(sock, server_hostname=host)
sock.send(smuggle_request.encode())
response = sock.recv(4096).decode()
print(response)
sock.close()
## Should print "400 Bad Request" in the response
Check Nginx Logs for Blocked Attempts
# Watch for 400 errors from smuggling attempts
tail -f /var/log/nginx/access.log | grep " 400 "
Summary Checklist
| Test | Expected Result |
|---|---|
nginx -t |
syntax ok |
Request with both CL + TE
|
400 Bad Request |
Request with only Content-Length
|
Normal response |
Request with only Transfer-Encoding
|
Normal response |
| Burp Suite smuggling scan | No vulnerabilities |
Top comments (0)