The Poisoned Connection Pool: What HTTP Desync Means for Proxy-Gated Media
Source: lobsters
HTTP request smuggling has been in the vulnerability literature since Watchfire’s 2005 paper, but it spent years treated as an academic curiosity. That changed in 2019 when James Kettle published HTTP Desync Attacks: Request Smuggling Reborn at DEF CON 27, demonstrating the technique against CDN and proxy infrastructure at scale. Seven years later, a researcher documented an HTTP desync bug in Discord’s media proxy that let an attacker capture media requests from other users. The mechanism is well-understood at this point, but the specific context of Discord’s proxy makes the impact sharper than the generic case.
The Ambiguity at the Root of It
HTTP/1.1 persistent connections reuse a single TCP socket for multiple sequential requests. Both sides of the connection, the proxy forwarding requests and the server receiving them, must agree on exactly where each request ends. HTTP/1.1 offers two mechanisms for signalling body length: Content-Length, a plain byte count, and Transfer-Encoding: chunked, which frames the body in variable-size chunks terminated by a zero-length chunk.
RFC 7230, Section 3.3.3 is unambiguous: if a request contains both headers, Transfer-Encoding takes precedence and Content-Length must be stripped before forwarding. Servers that follow this rule have no problem. The vulnerability exists in implementations that do not follow it, and in proxy pipelines where the frontend and backend apply different rules to the same request.
The three canonical variants, labelled by which side honours which header:
- CL.TE: The frontend proxy reads
Content-Lengthand forwards a fixed byte count to the backend. The backend parses chunked encoding and stops at the zero-length terminal chunk, leaving the remaining forwarded bytes sitting in its read buffer. Those bytes become a prefix for the next request on that connection. - TE.CL: The frontend strips the content after the chunked terminal but the backend uses
Content-Length, which declares a longer body. The backend waits for more data, pulling in the beginning of the next request to satisfy the count. - TE.TE: Both sides nominally support chunked encoding but one can be confused by an obfuscated
Transfer-Encodingvalue. Common tricks includeTransfer-Encoding: xchunked, headers with extra whitespace, or duplicate headers. One side falls through toContent-Length; the other processes chunked normally.
A minimal CL.TE probe looks like this:
POST / HTTP/1.1
Host: target.example
Content-Length: 6
Transfer-Encoding: chunked
0
X
The frontend reads 6 bytes of body (0\r\n\r\nX) and forwards the whole request. The backend honours chunked encoding, reads the zero chunk as end-of-body, and considers the request complete. The trailing X sits in the TCP socket buffer. When the next request arrives on that keep-alive connection from any user, the backend prepends that X to it.
Why a Media Proxy Is a Better Target Than a Generic Reverse Proxy
Most desync demonstrations end with session cookie theft or cache poisoning of a static resource. Discord’s media proxy is a more interesting target for a specific reason: the URLs it handles are short-lived signed credentials.
In late 2023, Discord introduced expiring attachment URLs to limit hotlinking and protect private content. Every media URL contains a signature and an expiry timestamp embedded as query parameters. A URL pointing to a private attachment in a DM or restricted channel is structurally equivalent to a session token: it grants time-limited access to specific content, and it’s tied to the identity of the user who requested it.
When an attacker captures a victim’s request prefix through desync, they receive that full URL including its signature and path. That path identifies the file, and the path structure for Discord attachments encodes the channel ID and message ID. Even setting aside whether the attacker can replay the URL before it expires, the captured request leaks which channel the victim was reading and which message contained the attachment. For users sharing sensitive files in private DMs, that metadata alone is a privacy violation distinct from any access to the file content itself.
This is qualitatively different from stealing a session cookie, which requires the attacker to make authenticated API requests as the victim. A captured signed media URL can be used directly, without any Discord API interaction, for whatever validity window Discord’s token service allows.
How the Capture Works
The request-capture variant of desync, as Kettle describes it in the PortSwigger Web Security Academy, works by injecting a smuggled prefix that begins a partial request to an attacker-controlled endpoint. The attacker sends this poisoned request through the vulnerable proxy, leaving a partial POST body in the backend’s read buffer. When an innocent user’s request arrives on the same backend connection immediately after, the backend appends that request to the buffer, completing the body of the attacker’s partial request.
The attacker’s endpoint receives a request whose body contains the beginning of the victim’s original request: the method, path, headers, and whatever the backend forwarded. For a media proxy, that means the victim’s signed URL, any cookies the proxy forwarded, and request headers the client sent.
At Discord’s traffic volume, the window between injecting the prefix and a victim request landing on the same backend socket is short. Media proxies handle high request rates because serving images from a CDN involves many small, fast fetches. The probability of capturing a victim request within a few milliseconds of poisoning the connection is high compared to lower-traffic targets. Running the injection repeatedly makes the attack probabilistic but practical.
The HTTP/2 Downgrade Problem
HTTP/2 eliminates this class of vulnerability by design. Its binary framing layer assigns explicit byte boundaries to each message, removing the Content-Length versus Transfer-Encoding ambiguity entirely. RFC 9113 forbids Transfer-Encoding headers in HTTP/2 requests altogether.
But HTTP/2 only covers the client-to-proxy leg of most real infrastructure. Internal connections between proxy tiers, between CDN edge nodes and origin servers, between load balancers and application backends, are often still HTTP/1.1. The proxies performing H2-to-H1 translation at these boundaries reintroduce the ambiguity when they synthesise HTTP/1.1 requests from H2 frames.
Kettle documented this as H2.CL and H2.TE smuggling in 2021, showing that an attacker can craft HTTP/2 requests with pseudo-headers that, when translated to HTTP/1.1 by an edge proxy, produce the conflicting header combinations that enable desync. The attack surface moves inward rather than disappearing.
For Discord’s architecture, which involves separate CDN, media processing, and storage tiers each with their own proxy logic, the number of HTTP/1.1 internal hops multiplies the places where this can occur.
What Mitigation Looks Like in Practice
Stict header validation is the cheapest fix: reject any incoming request that contains both Content-Length and Transfer-Encoding. RFC 7230 requires this behaviour; implementing it at every proxy boundary closes the CL.TE and TE.CL vectors. The harder problem is TE.TE obfuscation, because the space of malformed Transfer-Encoding values that different implementations accept silently is large. A strict parser helps; a paranoid one rejects anything it does not recognise exactly.
Disabling connection reuse between proxy tiers eliminates the attack surface entirely, because a smuggled prefix has nowhere to sit if there is no persistent socket to another client’s request on the other end. At high traffic volumes this is expensive: persistent connections and connection pooling exist specifically to reduce TCP handshake overhead. The tradeoff is real, which is why most large platforms prefer header normalisation over connection isolation.
End-to-end HTTP/2 is the correct long-term answer, but migrating all internal components to speak H2 is rarely a short project. Internal services written against older HTTP/1.1 client libraries, binary protocol parsing differences, and the sheer number of internal hops in a large distributed system all create inertia.
PortSwigger’s HTTP Request Smuggler Burp extension automates detection by sending timed probe requests and observing whether a server holds connections open waiting for data that the attacker’s crafted request implied was coming. The detection works without full exploitation, but it requires access to both sides of the proxy boundary to be conclusive, which means routine penetration testing of individual endpoints will not find it.
Why This Keeps Happening
The recurring theme in desync postmortems is not negligence; it is architectural. The vulnerability lives at the boundary between two systems, and each system in isolation behaves correctly according to its own implementation. A proxy that normalises Transfer-Encoding headers before forwarding has no bug. A backend that strictly enforces RFC 7230 has no bug. The bug is the combination, and the combination is the result of two teams making independent decisions about how to handle edge cases in a protocol that permits ambiguity.
At an organisation the size of Discord, with separate teams owning different layers of media infrastructure, a cross-tier protocol audit requires someone to ask the right question: do your proxy tiers agree on how they parse ambiguous HTTP/1.1 headers? Routine code review does not surface this. Standard application security scanning does not find it. It requires either dedicated protocol-level testing with tooling like Request Smuggler, or an external researcher asking the question on a bug bounty programme.
The fact that Discord patched this and that the researcher responsibly disclosed it through HackerOne is the expected outcome. The more durable lesson is that proxy infrastructure needs the same kind of adversarial protocol testing that application code gets for injection vulnerabilities. A media proxy handling private user content is not passive plumbing. It is a security boundary, and boundaries between systems with different protocol interpretations are where the interesting bugs live.