Most write-ups about HTTP desync lead with the header ambiguity: Content-Length versus Transfer-Encoding: chunked, two ways to declare where a request body ends, one proxy honouring each. That framing is accurate, but it undersells the mechanism that makes the attack dangerous in practice. The header disagreement is the setup. Connection pooling is the weapon.
A researcher recently published a detailed account of an HTTP desync bug in Discord’s media proxy. The vulnerability allowed an attacker to intercept arbitrary media requests from other users on the platform. Reading it with some background in how Discord’s infrastructure works, and in how HTTP/1.1 persistent connections actually behave, makes the attack substantially easier to reason about.
Why Connection Reuse Matters
HTTP/1.1 introduced persistent connections as a performance optimisation. Without them, every request-response pair requires a TCP handshake, a TLS negotiation, and then teardown. At scale, that overhead is prohibitive. So modern proxies maintain a pool of long-lived sockets to upstream backends and reuse them across many client requests.
The consequence is that a single TCP socket to a backend serves requests from many different, unrelated clients in sequence. The backend reads one request, writes a response, reads the next, and so on, all on the same file descriptor. Correct operation depends entirely on both sides agreeing, byte for byte, on where each request ends.
When they disagree, the leftover bytes from one request sit in the socket’s read buffer, waiting. The next client request that arrives on that same connection gets those bytes prepended to it before the backend sees it. The backend now parses a request that begins with whatever the attacker left behind.
This is what HTTP desync exploits. The header confusion is just the mechanism for making one side read fewer bytes than the other; the connection pool is what turns that discrepancy into cross-user interference.
The HTTP/1.1 Ambiguity in Concrete Terms
HTTP/1.1 allows a request to carry both a Content-Length header and a Transfer-Encoding: chunked header. RFC 7230, Section 3.3.3 says Transfer-Encoding takes precedence and Content-Length must be ignored when both are present. The specification is unambiguous. The implementations are not.
James Kettle’s HTTP Desync Attacks research, presented at DEF CON 27 in 2019, systematically catalogued what happens when proxies and backends diverge on that rule. The two main variants:
CL.TE: The front-end proxy trusts Content-Length and forwards that many bytes to the backend. The backend trusts Transfer-Encoding: chunked and reads until a zero-length terminating chunk. An attacker sets Content-Length to cover the entire payload, including a suffix after the chunked terminator, while structuring the chunked body to end early. The backend consumes the chunked portion and leaves the suffix in the buffer.
POST / HTTP/1.1
Host: media.discordapp.com
Content-Length: 6
Transfer-Encoding: chunked
0
X
The proxy forwards six bytes. The backend reads the zero chunk as end-of-body and leaves X in the buffer. Substitute a partial HTTP request for X and you have a smuggled prefix.
TE.CL: The reverse. The front-end uses Transfer-Encoding, the backend uses Content-Length. The attacker crafts a chunked body where the first chunk contains fewer bytes than the declared Content-Length, so the backend reads only that first chunk’s worth of data and treats the rest as the beginning of the next request.
A third variant, TE.TE, involves both sides nominally supporting chunked encoding but one of them being confused by a malformed or obfuscated Transfer-Encoding value, such as Transfer-Encoding: xchunked or a value with extra whitespace. The confused side falls through to Content-Length; the other processes chunked normally. This variant matters because it bypasses defences that only check for the canonical chunked string.
What Discord’s Attachment URLs Reveal
I work with Discord’s API regularly and the structure of its media URLs is something I have dealt with in bot code often enough to recognise what a captured request would actually expose.
Discord attachment URLs follow a predictable schema:
https://cdn.discordapp.com/attachments/{channel_id}/{attachment_id}/{filename}
or through the media proxy:
https://media.discordapp.net/attachments/{channel_id}/{attachment_id}/{filename}
The channel_id and attachment_id are Discord snowflakes, 64-bit integers that encode the creation timestamp in their high bits. Knowing a channel ID tells you which channel a user was reading. For public servers, that is low-value. For a private DM channel or a restricted private server channel, it tells you which specific conversation the victim was accessing.
Discord began signing attachment URLs in late 2023 with time-limited tokens appended as query parameters, after external links to Discord attachments were being scraped. A captured signed URL carries that token, which remains valid until expiry, meaning an attacker who captures the URL can also fetch the actual media content within the validity window.
The combination of channel ID, attachment ID, filename, and a valid signed token is enough to know what a specific user was looking at, which conversation it came from, and to download the file. For a platform where people exchange medical records, legal documents, and private photographs in DMs, that primitive is serious.
HTTP/2 Does Not Save You Here
HTTP/2 eliminates Transfer-Encoding entirely. Its binary framing layer encodes message boundaries directly in the protocol, making CL.TE and TE.CL attacks structurally impossible on a pure H2 connection. The natural conclusion is that migrating to H2 at the edge closes this class of vulnerability.
It does not, if the edge downgrades to HTTP/1.1 when talking to the backend. Most large platforms run this topology: H2 between clients and the load balancer, HTTP/1.1 between the load balancer and the application tier, because many internal services do not speak H2 natively.
Kettle documented the H2-to-H1 variant in follow-up research, labelled H2.CL and H2.TE. An attacker sends an HTTP/2 request with a body or pseudo-header crafted so that when the edge translates it to HTTP/1.1 for the backend, the resulting HTTP/1.1 request contains an ambiguous Content-Length and Transfer-Encoding combination. The H2 edge thinks it forwarded one well-formed request; the HTTP/1.1 backend sees the same ambiguity as before.
For a platform like Discord with multiple internal proxy tiers, a CDN layer, media processing services, and storage backends, the H2-to-H1 boundary likely exists at multiple points in the request path. Each one is a potential desync surface.
Why Standard Security Testing Misses This
A conventional web application security assessment tests individual endpoints: send a request, observe a response, look for injection points, check authentication, verify authorisation. That model does not find desync.
Desync requires observing the parse state of a TCP connection shared between two systems, and the disagreement between those two systems about protocol semantics. Neither system is individually buggy. A unit test of the proxy’s header parsing passes. A unit test of the backend’s header parsing passes. The vulnerability only exists at the boundary between them.
Discover it requires either reading both implementations carefully enough to notice they differ on RFC 7230 compliance, or sending crafted requests and observing timing anomalies, specifically whether a request hangs waiting for bytes that the server believes are still coming as part of a previous request’s body.
PortSwigger’s HTTP Request Smuggler Burp extension automates the probing side. It sends timing-based and differential probes and can detect desync behaviour without fully exploiting it. But running it requires knowing to look for this class of vulnerability in the first place, and it requires doing so across the actual proxy boundary in a production-realistic environment, not against individual services in isolation.
The Practical Mitigation Picture
The cleanest fix is to eliminate HTTP/1.1 between all internal tiers. End-to-end HTTP/2 removes the ambiguity at the protocol level. This is also the hardest fix, because it requires every internal service to support H2, which is often a multi-year migration for organisations with legacy backend infrastructure.
Short of that, the options are:
- Reject, at the edge, any request carrying both
Content-LengthandTransfer-Encoding, including obfuscated variants likeTransfer-Encoding: xchunked. The OWASP guidance recommends this as a baseline. - Disable connection reuse between proxy tiers, accepting the performance cost. A fresh TCP connection per backend request gives smuggled bytes nowhere to sit.
- Validate that every request forwarded between proxy tiers is well-formed before forwarding, normalising rather than passing through ambiguous headers.
Discord patched the issue through their HackerOne bug bounty program after the researcher reported it. The specific remediation they applied is not public, but at a platform that handles millions of media requests per minute, disabling connection pooling was almost certainly not the path they took.
The lasting lesson is that the proxy layer is never passive plumbing. Every hop between a client request and the backend that ultimately serves it is an active participant in interpreting protocol semantics. When those interpretations diverge, the gap belongs to whoever finds it first.