HTTP Request Smuggling and Desync
Where Does the Request End?
HTTP Request Smuggling and Desync
HTTP request smuggling starts with a mismatch: a front-end and a back-end disagree about where a request ends. On a reused connection, bytes left over from one request can become part of the next user’s request. This post explains how that happens, what an attacker can do with it, and how to test and defend against it. It’s the same parser-difference problem as DOM Clobbering and Mutation XSS, this time between HTTP servers.
The disagreement
In the previous post, DOM Clobbering and Mutation XSS came down to different parts of a browser interpreting the same input differently. Request smuggling is a similar mismatch between HTTP components: usually a proxy, load balancer, or CDN and the back-end server behind it. Here, the input is a stream of bytes sent over a reused TCP connection.
Modern setups almost never let the origin be accessed directly. Requests go through at least one intermediary, and for performance that intermediary keeps a pooled connection to the origin and sends many users’ requests through it one after another. All of that depends on both sides agreeing, byte for byte, about where each request ends.
If they disagree by even a few bytes, the remaining bytes from an attacker’s request become the first bytes of the next user’s request. The attacker never touches the victim. They simply cause the two parsers to count differently.
Both sides need to agree on the boundary. When they don’t, leftover bytes can be mistaken for the start of another request.
How HTTP/1.1 marks a boundary
An HTTP/1.1 message does not have a fixed length. The body is separated in one of two ways, and the whole set of problems lives in the space between them.
Content-Length: count the bytes
The simplest method states the body length in bytes using a Content-Length header. The receiver reads that many bytes and then considers the message finished.
Transfer-Encoding: chunked: read until a zero chunk
The streaming method sends the body in a series of chunks. Each chunk has a hexadecimal length, a carriage-return line feed, that many bytes, and another line feed. A zero-length chunk ends the body. The receiver does not know the total length beforehand; it reads until it sees the final 0 chunk.
```http id=”3a2zqb” Transfer-Encoding: chunked
b (next chunk is 0xb = 11 bytes) hello world (11 bytes of data) 0 (zero-length chunk: body ends here) (trailing CRLF closes the message)
### The rule and why it is not enough
The specification is clear: if a message contains both a `Content-Length` header and a `Transfer-Encoding: chunked` header, `Transfer-Encoding` wins. The conflicting `Content-Length` cannot be used to determine the message boundary, and the message may instead be rejected [3][4].
A single parser that follows the rules can handle this correctly. The problem starts when a request goes through two parsers. Those parsers might have been written by different teams, in different programming languages, or even years apart. One parser might follow the rule exactly. The other might contain a bug, use a shortcut, or read the rules more strictly or loosely. Now the same bytes can have two different boundaries.
The header that marks the end of a message is itself data that an attacker can send. When two servers do not agree on which delimiter to trust, the attacker can influence where the message ends on each server separately.
## Common desync patterns
Practical smuggling can be described by a few simple patterns. These patterns are named for the header that the front-end (F) and back-end (B) each trust.
### CL.TE: front-end counts bytes, back-end reads chunks
The front-end uses the `Content-Length` header and forwards the entire request. The back-end uses `Transfer-Encoding`, sees a zero chunk early, and considers the request finished. Anything the front-end sent after that zero chunk ends up in the back-end's buffer as orphaned data and is added before the next request on the same connection.
```http id="g6trxp"
POST / HTTP/1.1
Host: target.example
Content-Length: 13
Transfer-Encoding: chunked
0
SMUGGLED
The front-end forwards 13 bytes, including SMUGGLED. The back-end stops reading at the 0 chunk. SMUGGLED stays in the buffer and becomes the beginning of the next user’s request.
TE.CL: front-end reads chunks, back-end counts bytes
This is the mirror image. The front-end respects the chunked encoding and forwards the entire chunked stream. The back-end trusts the Content-Length header, reads only the number of bytes specified, and leaves the rest of the chunked data unread in the buffer. That leftover data can poison the next request.
TE.TE: both read chunks, but one is fooled
Both the front-end and back-end support chunked encoding, so the attacker cannot simply rely on one side ignoring the Transfer-Encoding header. Instead, the attacker hides or changes the header in a way that causes one side to miss it and silently fall back to Content-Length.
These obfuscations are small and come in many forms: a space before the colon, a tab instead of a space, an unexpected value like xchunked, a duplicated header, or a leading character. Each form targets differences in how a parser handles unusual input.
```http id=”wtqx1v”
Transfer-Encoding : chunked (space before colon)
Transfer-Encoding:
One parser treats these variations as valid chunked encoding while the other does not.
### H2.CL and H2.TE: the downgrade reopens the wound
HTTP/2 was meant to solve this problem. It uses binary framing and does not depend on `Content-Length` or `Transfer-Encoding` to mark request boundaries in the same way HTTP/1.1 does.
The problem reappears when a front-end talks HTTP/2 to the client but changes the request to HTTP/1.1 before sending it to the back-end. If that front-end rewrites the request into HTTP/1.1 while accepting an attacker-controlled `Content-Length` or `Transfer-Encoding`, or if it fails to reject carriage-return or line-feed characters hidden inside HTTP/2 header values, the attacker can again control where the HTTP/1.1 message ends on the back-end.
This type of attack, which also includes request splitting through CRLF injection into HTTP/2 headers, is covered in later research on HTTP desynchronization [2].
## What an attacker can do with a smuggled prefix
A smuggled prefix is a building block, not a full exploit. Its power comes from attaching data to a request that the attacker never sent. The common results are:
- **Bypassing front-end controls.** Access controls, WAF rules, and path restrictions enforced at the front-end are applied to the request that the front-end saw. The smuggled prefix is put together at the back-end, so it can reach paths that the front-end would have blocked.
- **Capturing another user's request.** By inserting a prefix that forces the next request's data to appear somewhere the attacker can read back, such as a comment field or reflected search field, the attacker can capture the victim's headers, including session cookies and authorization tokens.
- **Web cache poisoning and response queue desync.** A desync can shift the order of requests and responses on a connection, causing one user to receive another user's response or causing a poisoned response to be cached and served to everyone.
- **Turning a reflected input into a stored, cross-user effect.** Because the prefix travels with victims' requests, a low-value reflected issue can become a way to attack other users sharing that back-end connection.
## Detecting desync safely
Confirming a desync can corrupt real users' requests, so detection should favor methods that show the disagreement without leaving a poisoned prefix in a shared buffer. The main technique is timing, formalized in the desync research that revived the class [1].
### Timing a CL.TE target
Send a request whose framing forces the back-end to wait for bytes that will never arrive. If the front-end uses `Content-Length` and forwards a fixed number of bytes, but the back-end uses `Transfer-Encoding` and is left mid-chunk expecting more, the back-end blocks until it times out.
That delay is the signal. A matched control request with normal framing should return quickly, which helps separate the framing issue from ordinary network delays.
```http id="ojrbmm"
POST / HTTP/1.1
Host: target.example
Transfer-Encoding: chunked
Content-Length: 4
1
A
X
The front-end (CL: 4) forwards the bytes and stops. The back-end (TE) is left with an incomplete chunk and waits for the rest. A long, repeatable delay compared with a clean control is the desync signal.
Differential responses
When timing is inconclusive, a confirmed desync can create a difference that an attacker can see directly: a follow-up request returns a status or body that only makes sense if a prefix attached to it. Because this method can affect real traffic, it belongs in an authorized test window against a controlled account, not in casual probing.
What defenders can watch in telemetry
- Requests carrying both
Content-LengthandTransfer-Encoding, or malformedTransfer-Encodingvalues. In a normal client population these should be rare and are worth investigating. - Back-end request lines that do not match any front-end-issued request. This can be a sign of a prefix assembling at the origin.
- Spikes in back-end read timeouts correlated with specific source addresses, which can be the footprint of timing probes.
Testing and defending against desync
Testing should establish whether two parsers disagree and what that lets an attacker reach. Defenders should check that requests are parsed consistently and that leftover bytes cannot spill into another user’s request.
Testing
- Map the chain first. Identify every intermediary: CDN, WAF, load balancer, service mesh sidecar, and origin. Each hop is a parser, and each parser pair is a possible place for disagreement.
- Probe by timing, not by poisoning. Start with the timing method so detection does not interfere with real traffic. Confirm with a controlled differential only when authorized.
- Do not update Content-Length automatically. Turn off the client’s automatic header rewriting or the ambiguity you are creating may be removed before it leaves the tool.
- Chain it to real impact. A confirmed desync is a finding. A captured session or bypassed access control shows the actual impact. Trace the prefix to what it can affect.
Defending
- Normalize or reject at the edge. The front-end should reject requests that carry conflicting framing information or a non-conformant
Transfer-Encodinginstead of forwarding them. - Make both ends parse identically. Where possible, use the same server software and configuration for the front-end and back-end so there is less room for a parser differential.
- Break the shared buffer. Disabling reuse of back-end connections removes the way a leftover prefix reaches another user’s request. There is a performance cost, but in some environments that tradeoff may be worth it.
- Speak HTTP/2 to the origin. End-to-end HTTP/2 removes the HTTP/1.1 framing ambiguity rather than trying to filter every way it can be exploited.
Signals
| Signal | What it indicates |
|---|---|
| Timing delta between a mid-chunk probe and a clean control | Primary safe detection signal |
Coexisting Content-Length and Transfer-Encoding |
Ambiguity by construction |
Obfuscated Transfer-Encoding (space, tab, junk value, duplicate) |
TE.TE candidate |
| HTTP/2 front-end downgrading to HTTP/1.1 at the origin | H2.CL / H2.TE surface |
| Pooled, reused back-end connections shared across users | The channel a prefix travels on |
Reducing the risk by design
The most reliable defenses make every component agree on the request boundary, or prevent bytes left over on one request from reaching another.
End-to-end HTTP/2 removes the ambiguity outright
HTTP/2 frames messages in the protocol itself using binary frames. It does not rely on Content-Length or Transfer-Encoding to mark request boundaries in the same way HTTP/1.1 does. When HTTP/2 is used through every hop to the origin, the traditional CL and TE framing ambiguity is gone.
The remaining risk is the downgrade. When a hop converts the request to HTTP/1.1, the old framing rules come back, and that hop becomes responsible for producing valid, unambiguous HTTP/1.1. The goal is not just to offer HTTP/2 at the edge. It is to avoid downgrading behind it when possible.
Strict rejection makes the boundary singular
Where HTTP/1.1 must be used, the front-end can enforce that a request has exactly one valid way to be framed. Reject messages carrying conflicting framing headers, malformed or unsupported Transfer-Encoding values, embedded carriage returns or line feeds in header values, and duplicate framing headers.
This does not try to decide which of two interpretations is correct. It prevents input with two interpretations from entering the system in the first place.
```text id=”42dvaz” Reject, do not forward, when:
has(Content-Length) && has(Transfer-Encoding) conflicting framing Transfer-Encoding is malformed or unsupported obfuscation header value contains CR or LF splitting duplicate framing headers TE.TE / CL.CL ```
Connection-per-request removes the delivery channel
Even a successful desync needs a shared connection to carry the orphaned prefix to another request. If the front-end opens a new connection to the back-end for every request, leftover data has no next request to attach to and dies with the connection.
This trades some performance for isolation and can be a strong mitigation where the performance cost is acceptable.
Homogeneous parsing removes the differential
The problem exists because two independently built parsers can disagree. Reducing the number of different HTTP implementations in the chain, and configuring the remaining ones consistently, reduces the places where a disagreement can hide.
The connection to the previous post is the parser mismatch: browsers can disagree about markup, and HTTP components can disagree about message boundaries. In either case, consistent parsing is more reliable than trying to anticipate every unusual input.
Takeaway
Request smuggling is more dangerous than its simple mechanism suggests. The mechanism is only a few bytes of disagreement about where a message stops. The result can be the ability to rewrite the beginning of someone else’s request, turning a framing issue into a cross-user attack: stolen sessions, bypassed controls, poisoned caches.
The same setup that makes the web fast can create the conditions for this bug: independent intermediaries share pooled connections and may parse requests differently.
For a security team, the rules are fairly simple. List every hop and treat each pair of parsers as a place where two answers might diverge. Detect by timing before risking a poisoning probe, and do not let your tools automatically correct the ambiguity you are testing. On defense, reject ambiguous framing at the edge, prefer end-to-end HTTP/2, break shared back-end connections where the performance tradeoff makes sense, and reduce unnecessary parser differences throughout the chain.
Every hop needs to agree on where a request ends. Reject ambiguous framing, avoid risky downgrades, and review each parser in the request path.
References and further reading
Note on citations: These references should be verified against the primary sources before publication. Titles, authors, venues, years, and other bibliographic details should be confirmed at the source.
- Kettle, J. “HTTP Desync Attacks: Request Smuggling Reborn.” PortSwigger Research, presented at Black Hat USA and DEF CON, 2019. The work that revived the class and introduced the timing-based detection methodology.
- Kettle, J. “HTTP/2: The Sequel is Always Worse.” PortSwigger Research, Black Hat USA, 2021. HTTP/2 downgrade desync, H2.CL and H2.TE, and request splitting via header injection.
- Linhart, C., Klein, A., Heiled, R., Orrin, S. “HTTP Request Smuggling.” Watchfire, 2005. The original description of the technique.
- Klein, A. Subsequent research on HTTP request smuggling and response splitting across HTTP versions.
- IETF. RFC 9112, “HTTP/1.1” (obsoleting RFC 7230), message framing,
Content-LengthandTransfer-Encodingprecedence and rejection rules. - IETF. RFC 9113, “HTTP/2,” binary framing and message length semantics.
- PortSwigger Web Security Academy. HTTP request smuggling labs and the HTTP Request Smuggler Burp extension.