DEV Community

Davi
Davi

Posted on Originally published at blog.mago.team

Working: HTTP request smuggling is a distributed systems trust failure, not a web app bug

A single HTTP request reaches HAProxy. HAProxy reads the Content-Length, forwards 80 bytes, and considers the transaction closed. Apache receives those 80 bytes plus 42 additional bytes that it interprets as the beginning of the next request from a legitimate user. The attacker wrote those 42 bytes.

Request smuggling is not a web application vulnerability. It is a trust failure in distributed systems: each node in the proxy chain assumes the previous node correctly parsed the request boundary, and no specification can guarantee that assumption. RFC 7230, section 3.3.3, documents the gap explicitly without closing it. That 1999 decision still feeds CVEs with CVSS 10.0 in 2022 and large-scale account hijacking on platforms like Slack.

RFC 7230 section 3.3.3 is the vulnerability, not the implementation

The parsing ambiguity is not a bug in HAProxy or Apache. It is a predicted consequence of the HTTP/1.1 specification.

RFC 7230, section 3.3.3 (later superseded by RFC 9112, ยง6.3, with identical normative language -- RFC 7230 was retired in June 2022), defines that when both Transfer-Encoding and Content-Length are present in the same request, Transfer-Encoding takes precedence and Content-Length must be ignored. The core problem follows: the spec states that receiving both headers in a request "ought to be handled as an error" and that the server SHOULD reject the request. SHOULD is non-normative language. Each implementation is legally free to make a different choice.

Edge proxies prioritize availability and parse permissively to avoid discarding legitimate traffic with slightly malformed headers. Backends prioritize correctness and parse strictly. This systematically divergent behavior is not an accident: it is a guaranteed consequence of the spec gap.

The ambiguity has existed since RFC 2616, published in 1999, and survived intact through the RFC 7230 revision in 2014 without becoming normative language. Modern proxies like Pingora (Cloudflare's Rust-based proxy, open-sourced in 2023) still inherit the same problem because the architectural pattern remains unchanged: shared connection pools with TCP socket reuse across multiple users. When the specification does not force parsing convergence, connection reuse turns interpretation divergence into cross-user contamination.

CL.TE and TE.CL: which hop misparses and what that costs

CL.TE and TE.CL are not symmetric variants. They differ fundamentally in which node is the trusted partner, which is exploited, and where the attacker's bytes land in the backend socket.

In CL.TE, the edge proxy uses Content-Length. It forwards exactly N bytes and closes its view of the request. The backend uses Transfer-Encoding: chunked, reads the declared chunk, and waits for the 0\r\n\r\n terminator to know the body ended. The attacker's remaining bytes sit in the socket and are read as the beginning of the next request from any user sharing that backend TCP connection.

POST / HTTP/1.1
Host: vulnerable-site.com
Content-Length: 13
Transfer-Encoding: chunked

0

SMUGGLED-PREFIX
Enter fullscreen mode Exit fullscreen mode

In this payload, the proxy reads Content-Length: 13 and forwards 13 bytes (0\r\n\r\nSMUGGLED). The backend reads chunk 0 and interprets SMUGGLED-PREFIX as the start of the next request received on that socket. Any user whose request arrives next on that connection will have their request prefixed with the attacker's content.

In TE.CL, the roles reverse. The proxy uses Transfer-Encoding and forwards the complete chunked body, including the smuggled prefix. The backend uses Content-Length, stops after N bytes, and the remainder of the body sits in the socket as a prefix for the next request arriving on that connection.

CVE-2019-18277 shows that a single whitespace difference can completely change which parsing path HAProxy takes. HAProxy's legacy decoder ignores Transfer-Encoding headers containing vertical tab (\x0b) or form feed (\x0c). HAProxy falls back to Content-Length. Apache, at that same moment, accepts the malformed TE and prioritizes it over CL. CL.TE is active with no modification other than a single control byte in the header. CVSS 7.3, affecting HAProxy 1.7.9 through 2.0.5 in legacy mode.

Queue poisoning, cache deception, and CVSS 10.0 in SAP

Response queue poisoning, cache deception, and WAF bypass are distinct outcomes of the same desync primitive. Each has a different impact chain, but all begin with the same boundary misaligned between infrastructure nodes.

In queue poisoning, the backend sends an attacker-controlled response to the next user arriving on the same TCP connection. That response can contain arbitrary Set-Cookie headers or redirects. HackerOne #737140 documents the most detailed chain: CL.TE on a Slack subdomain, response queue poisoning, and session hijacking of random users with no victim interaction. The report was made public after the fix was applied.

Cache deception uses the same primitive to force the cache to store a private response under a public URL. Any user who later GETs that URL receives the original victim's data. WAF bypass injects the payload directly into the backend socket, invisible to all perimeter controls: the smuggled request never passes through the edge security inspection.

CVE-2022-22536 puts these attacks at enterprise scale. SAP NetWeaver, SAP Content Server, and SAP Web Dispatcher are affected: an unauthenticated attacker can prefix arbitrary data to requests from privileged SAP users, enabling session hijacking without credentials. CVSS 10.0, exploited in production, listed in the CISA Known Exploited Vulnerabilities catalog. James Kettle documented over $70,000 in bug bounties with the same vector in 2019, including $38,900 paid by PayPal for a single smuggling chain.

HTTP/2 did not fix this: H2.CL and H2.TE

HTTP/2 uses binary framing with explicit frame length. The Content-Length vs Transfer-Encoding ambiguity does not exist at the H2 protocol layer. That argument is technically correct and operationally incomplete.

Nearly every production HTTP/2 front-end downgrades to HTTP/1.1 when reaching the backend. That translation layer reintroduces the full desync surface and adds a new injection vector via HTTP/2 pseudo-header manipulation. Kettle presented H2.CL and H2.TE at DEF CON 29 in 2021, showing that previously safe sites under pure HTTP/1.1 became vulnerable after adding HTTP/2 termination at the CDN.

In H2.CL, the attacker injects a Content-Length header into the HTTP/2 request with a value different from the actual H2 frame length. The front-end uses the H2 frame length to process the request. After the downgrade to HTTP/1.1, the backend reads the injected Content-Length. In H2.TE, the attacker injects Transfer-Encoding: chunked into the HTTP/2 request. The front-end ignores the header because it operates with binary framing. The header survives intact through the downgrade process and flips the backend's parsing to chunked.

The downgrade itself is the attack surface, not a misconfiguration. No mechanism exists in the HTTP specification to synchronize request boundaries between an H2 hop and an HTTP/1.1 hop, because the boundary in H2 is the frame length and in HTTP/1.1 it is Content-Length or Transfer-Encoding. The translation layer creates a different boundary at each hop with no equivalence guarantee.

Detection: 3 independent signals, none exposes victims

Detecting desync between nodes is detecting the trust gap in operation.

Timing differentials, differential responses, and response cloning artifacts are independent signals that, used together, eliminate false positives and cover both CL.TE and TE.CL variants without affecting other users.

For CL.TE, the tester sends a request where Content-Length is satisfied but the TE chunk terminator is deliberately absent. If the backend uses Transfer-Encoding, it waits indefinitely for the next chunk. The result is a 10 to 30 second timeout observable only by the tester.

POST / HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: chunked
Content-Length: 4

1
A
X
Enter fullscreen mode Exit fullscreen mode

In this request, Content-Length: 4 satisfies the front-end (bytes 1\r\nA\r\n). The backend, using Transfer-Encoding, reads chunk 1 with content A, then reads X as the start of a new chunk waiting for its length. The 10.1-second delay confirms the backend uses Transfer-Encoding. No other users are affected because the request remains pending only on the tester's connection.

For confirmation without exposing victims, the tester sends two sequential requests on the same connection. If the second request returns content shaped by the smuggled suffix of the first, desync is confirmed. Response cloning as a production artifact indicates active queue poisoning: the victim receives a response with attacker-controlled content in the body.

Burp Scanner automates the timing-based check for CL.TE and TE.CL. The HTTP Request Smuggler extension covers deeper variants, including H2.CL and H2.TE. Reconnaissance pipelines available at intel.mago.team integrate smuggling detection as part of proxy surface analysis at scale.

The fix is architectural. Enforcing HTTP/2 end-to-end eliminates the downgrade layer that reintroduced the H2.CL and H2.TE vector. Strict rejection of TE+CL conflicts at each hop, not just at the edge, closes the gap that RFC 7230 left open for 25 years. Treating http-reuse always as a security control rather than a performance toggle removes the prerequisite that turns parsing desync into cross-user poisoning. The trust chain between proxies holds only when each node applies the same boundary with the same rule, and no specification can guarantee that for you.

Top comments (0)