HTTP Request Smuggling Defense: Nginx Reverse-Proxy Hardening
HTTP request smuggling exploits front-end/back-end parse differences to bypass WAF and access control. This guide covers CL.TE and TE.CL mechanics, full Nginx reverse-proxy hardening, and verification for ops and developers.
Scope
HTTP Request Smuggling hits reverse-proxy + backend app stacks. Nginx and backend Tomcat, Apache, or Java apps parse requests differently. Attackers craft headers so proxy and backend disagree on request boundaries, then bypass WAF, steal other users' requests, poison cache, or escalate. Applies to: sites using Nginx in front of Java/PHP apps, WAF deployments suspected of bypass, and teams that must meet MLPS Web application protection requirements.
Prerequisites
- Nginx 1.18+ as reverse proxy with proxy_pass
- Backend Tomcat, Jetty, or other Java app that supports Transfer-Encoding
- root on the server; can edit Nginx config and run nginx -t
- A test host for smuggling requests (commands verified on Linux)
Mechanism
Core issue: front-end proxy and backend disagree on request boundaries (Content-Length / Transfer-Encoding). Two primary techniques:
- CL.TE smuggling: front-end reads the body via Content-Length; backend prefers Transfer-Encoding: chunked. Attacker hides a second request in chunked data. Backend treats it as a standalone request and skips front-end WAF rules.
- TE.CL smuggling: front-end parses Transfer-Encoding: chunked; backend parses Content-Length. Length mismatch lets the backend assemble an unexpected request.
The smuggled second request can hit backend URLs that never saw the WAF: admin interfaces, cache poisoning, session fixation, or theft of user requests (real bugs include CVE-2019-20372, CVE-2020-10061). Defense: unify front-end/back-end parse behavior and close the smuggling channel at the source.
Procedure
Step 1: Detect smuggling risk
Confirm whether Nginx forwards the raw Transfer-Encoding header to the backend. Inspect current config:
`# Check whether Transfer-Encoding is handled explicitly
grep -rn "Transfer-Encoding" /etc/nginx/conf.d/ /etc/nginx/nginx.conf
# Use curl to test whether the backend responds to chunked (risk exists if the proxy forwards TE)
curl -sI http://your-backend-server/ -H "Transfer-Encoding: chunked" | grep -i "HTTP/"`
Step 2: Disable the smuggling channel at Nginx
In the server or location block, reject requests that carry Transfer-Encoding and enforce Content-Length uniqueness (key parameters):
`# /etc/nginx/conf.d/smuggling.conf
server {
listen 443 ssl;
# Reject requests that contain both CL and TE headers (smuggling prerequisite)
if ($request_headers ~* "(Content-Length|Transfer-Encoding)") {
if ($http_transfer_encoding) {
return 400;
}
}
# Preferred: use map for precise matching (avoid nested if)
}`
More solid: drop TE at the http layer so the backend always parses Content-Length:
`# /etc/nginx/conf.d/proxy_security.conf (http-layer include)
proxy_set_header Transfer-Encoding "";
# Enforce header uniqueness: reject dual CL headers immediately
more_clear_headers Transfer-Encoding;
# If the headers-more module is not installed, use Lua or apply the restriction at the server layer`
Step 3: Restrict methods and tighten sizes (recommended)
`server {
# Allow only methods required by the application; block CONNECT/TRACE and other common smuggling channels
if ($request_method !~ ^(GET|HEAD|POST|PUT|DELETE|OPTIONS)$) {
return 405;
}
# Limit request body and header size to shrink smuggling payload space
client_max_body_size 10m;
client_header_buffer_size 4k;
large_client_header_buffers 4 8k;
}`
Step 4: Backend and WAF joint hardening
- Backend Tomcat: on the Connector in server.xml, set maxHttpHeaderSize and rejectIllegalHeader (Tomcat 9.0.31+)
- WAF: enable OWASP CRS 920420 (header size) and 920310/920320 (request-smuggling signatures)
- Upgrade all components: Nginx 1.18.0+, Tomcat 9.0.40+ already fix most smuggling CVEs
Verification
`# 1) Syntax-check and reload
nginx -t && nginx -s reload
# 2) Verify requests with a TE header are rejected (should return 400)
curl -i http://your-site/ -H "Transfer-Encoding: chunked" --data-binary "0" | head -5
# 3) Verify dual CL header requests are rejected
curl -i http://your-site/ -H "Content-Length: 1" -H "Content-Length: 2" -X POST -d "a" | head -5
# 4) Normal requests should return 200; application traffic is unaffected
curl -s -o /dev/null -w "%{http_code}\\n" http://your-site/`
FAQ
FAQ 1: Will blocking the TE header break legitimate chunked uploads?
No. Nginx already handles the body with its own chunked decoder. The app does not need to see it. What is rejected is a client-supplied TE header. Normal browser uploads do not send it. If a client insists on chunked, normalize at Nginx and re-encode; do not forward the raw header.
FAQ 2: Why did my smuggling test return 200?
Nginx and the backend currently parse the same way (or Nginx already decoded chunked). Smuggling fails. That is the safe state. Still apply Step 4 and re-check component versions after upgrades — a backend upgrade can change parse behavior.
FAQ 3: Is Nginx-only config enough?
No. Smuggling is a front-end/back-end problem. Unify both ends: proxy intercepts, backend rejects illegal headers, WAF adds rules. All three required. Hardening one side can be bypassed by a new feature on the other.
Summary
HTTP request smuggling is front-end/back-end parse mismatch. Defense: unify parsing, block at the source. Nginx drops Transfer-Encoding, rejects dual Content-Length, restricts methods; pair with backend Connector settings and WAF rules to close the channel. Put this in the server security baseline. Re-verify after every component upgrade.