DEV Community

Cover image for Mitigating HTTP Request Smuggling
Aniket Bhatia
Aniket Bhatia

Posted on

Mitigating HTTP Request Smuggling

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:

  1. Use HTTP/2 end-to-end — HTTP/2 has a binary framing layer that eliminates ambiguity in request boundaries
  2. Normalize requests at the front-end — ensure the proxy resolves ambiguous requests before forwarding
  3. Disable connection reuse between front-end and back-end (performance trade-off but eliminates the attack surface)
  4. 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 a 0\r\n chunk"

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 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;
}
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 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)