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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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 URLWhy 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;
}
Enter fullscreen mode Exit fullscreen mode
No ambiguity = no smuggling.
Testing HTTP Request Smuggling Protection
Validate Nginx Config Syntax
sudo nginx -t
Enter fullscreen mode Exit fullscreen mode
Should output: syntax is ok and test is successful. Then reload:
sudo systemctl reload nginx
Enter fullscreen mode Exit fullscreen mode
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"
Enter fullscreen mode Exit fullscreen mode
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"}
Enter fullscreen mode Exit fullscreen mode
## 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"}'
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
Check Nginx Logs for Blocked Attempts
# Watch for 400 errors from smuggling attempts
tail -f /var/log/nginx/access.log | grep " 400 "
Enter fullscreen mode Exit fullscreen mode
Summary Checklist
Test Expected Resultnginx -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
답글 남기기