One of the sharpest outcomes of a request desync is capturing other users requests in full, their cookies, their headers, and their body, and reading all of it back later at your leisure. It turns a quiet boundary disagreement between two servers into a theft of whatever the next person in line was about to send. This post shows the technique concretely, with an invented app, so the mechanism is something you can see rather than something you have to take on faith.
If the groundwork is new to you, start with our primer on http request smuggling and then come back. Here we assume the desync already exists between a front end and a back end, and we focus on one question: how does an attacker arrange for a stranger’s bytes to land somewhere the attacker can read them?
Why capturing other users requests works
Recall the core of a desync. The front end and the back end share one connection and reuse it for many people. When they disagree about where a request ends, the back end is left holding attacker chosen bytes at the front of its buffer. Those leftover bytes become a prefix glued onto whoever sends the next request on that connection.
In the plainest smuggle, that prefix is a complete request the attacker authored, and the victim’s request is wasted driving it. Capture is a smarter use of the same prefix. Instead of ending the prefix as a finished request, the attacker ends it in the middle of a request to an endpoint that stores its input and later shows it back. The victim’s incoming bytes then flow into that unfinished body and become the stored value. The attacker reads them later like any other saved content.
The stored and reflected trick, step by step
Picture a team wiki we will call Acme Wiki. Acme Wiki has a comment endpoint, POST /comments, that saves whatever you submit and renders it back on a page you can revisit. That is an ordinary, harmless feature. It is also the perfect landing pad, because the stored and reflected behavior is exactly what the attacker needs: somewhere the captured bytes go in and later come out.
The attacker crafts a single request against the desync. The outer request is shaped so the front end treats it as one complete POST, while the back end stops early and keeps the rest as a waiting prefix:
POST / HTTP/1.1 Host: acme.example Content-Length: 256 Transfer-Encoding: chunked 0 POST /comments HTTP/1.1 Host: acme.example Content-Type: application/x-www-form-urlencoded Cookie: session=attacker-session Content-Length: 500 comment=
Read it as each server. The front end honors Content-Length and forwards the whole thing as one request. The back end honors Transfer-Encoding: chunked, sees the 0 chunk, decides that body is finished, and treats everything after it as the start of a new request. That new request is the smuggled POST /comments. It announces Content-Length: 500 but only a handful of bytes follow the comment=. So the back end waits. It needs 500 bytes of body and it does not have them yet.
Now the next user sends a normal request on that shared connection:
GET /dashboard HTTP/1.1 Host: acme.example Cookie: session=victim-session-here User-Agent: ...
The back end is still filling the body of the smuggled comment. It reads the victim’s request line, the victim’s Cookie header, and everything else as the remaining bytes of comment=. All of it is stored as a comment under the attacker’s own session. The attacker then opens the comments page and reads the victim’s full request, session cookie included.
Capture does not break the victim’s request by discarding it. It redirects the request into a box the attacker owns, so the victim’s own bytes become saved content with the attacker’s name on it.
What the victim loses
The captured value is a verbatim copy of an in flight request. That usually means the session cookie, which is enough to impersonate the victim outright. It can also mean a bearer token in an Authorization header, a CSRF token, or sensitive body content such as a password change or a private message. The attacker did not phish anyone and did not touch the victim’s device. They set a trap on a shared connection and waited for a request to walk into it.
A few practical details decide whether the trap catches anything useful. The Content-Length on the smuggled request has to be large enough to swallow the interesting parts of the victim request but not so large that the back end hangs waiting forever for bytes that never arrive. The storing endpoint has to accept the captured blob without rejecting it as malformed. And the attacker usually has to fire the smuggle repeatedly, because which victim request lands in the trap is a matter of timing on a busy connection.
How capture differs from the other outcomes
It helps to place capture next to its siblings so you do not confuse them. In response queue poisoning, the boundary is off by one and the victim receives a response meant for someone else, so the leak flows toward the victim. Capture flows the other way: the victim’s request flows toward the attacker. Both start from the same desync, often the same CL.TE request smuggling primitive, but the payload and the proof are different. This difference between finding a mismatch and proving a specific, reachable consequence is a theme across our injection and input category.
Shutting the trap
The fix is not to harden the comment endpoint. The endpoint did nothing wrong. The fix is to remove the desync that let a stranger’s bytes reach it. That means the front end and the back end must agree on request boundaries: reject any request that carries both Content-Length and Transfer-Encoding, close the connection after an ambiguous request so no prefix can wait, and normalize framing at the front end so the back end never has to guess. Not pooling back end connections across users shrinks the blast radius, because a leftover prefix then cannot land on a stranger.
Capture is a good illustration of why proof matters more than pattern matching. A header mismatch on its own is a maybe. A saved comment that contains a real victim session is proof that the desync is real, reachable, and worth fixing now. Reasoning from a dangerous outcome back to the exact conditions that make it reachable, then confirming it with hard evidence, is what an autonomous researcher that tests an app’s assumptions is built to do, and you can read how we approach that on our about page.
Frequently asked questions
How does a request desync end up capturing other users requests?
The attacker smuggles a prefix that ends in the middle of a request to an endpoint that stores and reflects its input, like a comment or profile field. The smuggled request declares a large Content-Length but supplies only a few body bytes, so the back end waits for more. The next user’s request fills those bytes, so their request line, headers, and cookies are saved as the attacker’s stored value and can be read back later.
Why does the smuggled request need a stored and reflected endpoint?
The captured bytes have to go somewhere the attacker can retrieve them. An endpoint that saves input and renders it back, such as a comment feed or a public bio, gives the victim’s request a place to land and a page to read it from. Without a stored and reflected sink the bytes would be consumed and lost, which is why the technique targets exactly that kind of feature.
What can an attacker do with a captured request?
A captured request is a verbatim copy of what the victim sent, so it usually contains the session cookie, which is enough to impersonate them. It can also expose an Authorization token, a CSRF token, or sensitive body content like a password change. No phishing or device access is needed, since the attacker only sets a trap on a shared connection and waits for a request to fall into it.
How do you stop requests from being captured this way?
Harden the connection, not the storing endpoint, because the endpoint did nothing wrong. Reject any request carrying both Content-Length and Transfer-Encoding, close the connection after an ambiguous request so no prefix can wait, and normalize framing at the front end so the back end never guesses a boundary. Not pooling back end connections across users shrinks the blast radius so a leftover prefix cannot land on a stranger.
Put an autonomous researcher on your own systems
UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.
