Category: Deep Dives

Long form technical deep dives into one mechanism at a time: cloud, kernel, IoT, and privacy internals.

  • Detecting HTTP Request Smuggling Safely

    Detecting HTTP Request Smuggling Safely

    detecting HTTP request smuggling is less about firing a payload and more about proving a careful claim: that two servers in a chain really do slice one byte stream at different points, and that the gap between them is reachable and exploitable. A header that looks suspicious is not a finding. The finding is a desync you have confirmed with a safe, specific piece of evidence. This post walks the methodology we trust, and why the order of steps matters.

    If the underlying mechanics are fuzzy, our primer on http request smuggling covers how a front end and a back end come to disagree about where a request ends. Here we assume you understand the mechanism and want to know how to confirm one in the wild without harming the people using the site.

    Why detecting HTTP request smuggling is a confirmation problem

    The reason this needs care is that the attack leaves no bad string to match. Every byte of a smuggling request is valid on its own. The vulnerability is entirely in how two parsers divide the same stream, which you cannot read off a single request or a single response. So detection cannot be a signature. It has to be an experiment: send something that behaves one way if the servers agree on boundaries and a different, observable way if they do not, then read the difference.

    That framing also sets the bar for a report. A difference in how two servers echo a header is a hint, not proof. What proves a desync is a response that can only be explained by one server reading bytes the other server thought belonged to a different request. Everything below is about producing that response on purpose, safely.

    Timing based probes

    The safest first probe is timing based, because it can confirm a desync without ever leaving a smuggled request waiting for a real user. The idea is to craft a request that, if the back end parses it the way you suspect, will leave the back end waiting for more body bytes that never come. A back end that is waiting holds the response. You see a delay.

    Consider a request whose front end reading finishes cleanly, but whose back end reading, under a Content-Length versus Transfer-Encoding disagreement, expects a further chunk:

    POST / HTTP/1.1
    Host: target.example
    Transfer-Encoding: chunked
    Content-Length: 4
    
    1
    A
    X

    If both servers agree, this returns promptly. If the back end honors Transfer-Encoding and is left expecting the next chunk after A, it waits for bytes that are not coming, and the response hangs until a timeout. A response that returns fast in the safe case and hangs in the desync case is a clean, self contained signal. Just as important, the probe is arranged so the bytes left over are consumed by the timeout, not parked in the buffer waiting to collide with the next visitor.

    A good first probe for a desync is one that hangs on your own connection if it is real and bothers nobody if it is not. You are asking a question the server can only answer slowly.

    Differential responses

    Timing tells you something is off. Differential responses tell you what. The method is to send two requests that are identical except for the one detail you think triggers the desync, and compare what comes back. If a request crafted to smuggle a prefix produces a different status, a different error, or a different body than its harmless twin, and the difference matches your model of how the boundary moved, you are narrowing a hint into a mechanism.

    The disciplined way to run this is to send the probe to yourself. Smuggle a prefix whose only effect is to change the response your own next request on that connection receives. If your follow up request comes back altered in exactly the way the smuggled prefix predicts, the desync is confirmed by your own traffic alone. You never needed a real user to complete the proof. An intercepting proxy that lets you control raw bytes and reuse a single connection is the tool for this; which one does not matter.

    Careful teardown, so you do not poison real users

    This is the step that separates responsible confirmation from reckless testing, and it is where most of the risk lives. A smuggle that works leaves a prefix in a shared back end buffer. If a real user’s request arrives before your own follow up, their request gets the prefix, and you have just affected someone who had nothing to do with your test. Detecting HTTP request smuggling responsibly means planning the cleanup before you send anything.

    • Prefer probes that self resolve. Timing probes that end in a timeout consume their own leftover bytes. They are the first choice precisely because there is nothing left to collide with another user.
    • Own both ends of the connection. When you must leave a prefix, send your completing request on the same connection immediately, so your own bytes claim the prefix and not a stranger’s.
    • Close the connection after a positive. Once a probe confirms a desync, close the connection so no residual state carries into later traffic on it.
    • Test gently and off peak. Fewer concurrent real requests means a far smaller chance of a collision during the brief window a prefix exists.

    These same leftover bytes are what make outcomes like response queue poisoning dangerous, which is exactly why a tester has to treat them as live and clean them up rather than leaving them for chance. The care you take during detection is a smaller version of the care the fix is supposed to enforce.

    Where confirmation beats guessing

    The thread through all of this is that a desync is either real and reachable or it is noise, and only an experiment can tell the two apart. Flagging a Content-Length and Transfer-Encoding that look mismatched, then guessing at impact, produces a pile of maybes that waste a defender’s time. A timing probe that hangs, a differential response that matches your model, and a clean teardown together produce one thing worth sending: a confirmed, reachable desync with a safe proof attached. This confirmation first stance runs through our whole injection and input category.

    Proving a desync, rather than reporting a suspicion, is the entire point of how UnboundCompute approaches a finding. It forms an idea about where a chain could break, designs an experiment that answers yes or no, and reports only once the evidence is in hand, which for a desync means a safe proof that the boundary really moved. That is the difference between signal and a guess, and it is what our about page describes.

    Frequently asked questions

    Why is detecting HTTP request smuggling a confirmation problem and not a signature?

    The attack leaves no bad string to match, because every byte of a smuggling request is valid on its own. The vulnerability is in how two parsers divide the same stream, which you cannot read off a single request or response. So detection has to be an experiment: send something that behaves one way if the servers agree on boundaries and a different, observable way if they do not, then read the difference.

    How does a timing based probe confirm a desync?

    You craft a request whose front end reading finishes cleanly but whose back end reading leaves the back end expecting more body bytes that never arrive. A waiting back end holds the response, so you see a delay. A probe that returns fast when the servers agree and hangs until a timeout when they disagree is a clean, self contained signal, and the leftover bytes are consumed by the timeout rather than parked for the next visitor.

    What are differential responses used for?

    Timing tells you something is off; differential responses tell you what. You send two requests identical except for the detail you think triggers the desync and compare the results. The disciplined version smuggles a prefix that only changes the response your own next request on that connection receives, so a matching change confirms the desync from your own traffic alone, with no real user needed to complete the proof.

    How do you test without poisoning real users?

    Plan the cleanup before sending anything. Prefer timing probes that end in a timeout and consume their own leftover bytes. When you must leave a prefix, send your completing request on the same connection immediately so your own bytes claim it, close the connection after a positive so no residual state carries over, and test gently and off peak to shrink the window a prefix exists.


    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.

  • Connection State Request Smuggling

    Connection State Request Smuggling

    connection state request smuggling is a desync that does not rely on two servers disagreeing about where a request ends. Instead it abuses a different shortcut: a front end that looks closely at the first request on a connection, makes a decision about trust or routing from it, and then applies that same decision to every later request on the same connection without checking again. The attacker sends a clean first request to earn the decision, then rides it with requests that should never have qualified.

    This sits alongside the length based variants in our primer on http request smuggling, so read that for the groundwork. The twist here is that the vulnerability lives in state the front end keeps about the connection, not in a header parsing mismatch.

    Connection reuse, stated plainly

    Start with the plumbing, because the bug is a shortcut taken on top of it. A front end does not dial a fresh connection to the back end for every request. It keeps a pool of connections open and sends many requests down each one, from many different users, one after another. The back end reads each connection as a stream of back to back requests.

    Validating and routing a request costs work. Parsing the Host, checking a client certificate, deciding which back end pool to use, running an access rule. A front end under load is tempted to do that work once per connection rather than once per request. That optimization is the whole vulnerability.

    First request routing turns one check into a free pass

    The clearest form is first request routing. The front end reads the first request on a new connection, decides from it which back end to forward to, and then pins every later request on that connection to the same destination. The later requests are never re examined. Their Host header, their path, their intended destination, none of it is consulted again, because the routing decision is cached on the connection.

    Now the attack writes itself. The attacker sends a first request to a host the front end is happy to route, say the public site:

    GET / HTTP/1.1
    Host: public.example
    
    

    On the same connection, the attacker sends a second request aimed somewhere the front end would normally refuse to route from the outside:

    GET /admin HTTP/1.1
    Host: internal.example
    
    

    The front end already decided this connection goes to the destination chosen by the first request. It forwards the second request there too, without checking that internal.example was never supposed to be reachable. The attacker reached an internal virtual host by spending one honest request to set the routing, then reusing it.

    The front end asked where does this go exactly once, trusted the answer forever, and the attacker made sure the one time it asked was the one request that would answer safely.

    How connection state request smuggling abuses cached trust

    Routing is only one decision a front end might cache on a connection. The pattern generalizes to anything validated once and reused.

    • Client certificate authentication. If the front end checks a client certificate when the connection is established and then treats every request on it as authenticated, an attacker who can get requests onto an already authenticated connection inherits an identity they never proved.
    • Authorization decisions. A front end that authenticates the first request and marks the connection as belonging to that user can let a later request act as that user, even one the attacker injected.
    • Protocol and header normalization. A front end that decides a connection is well behaved from its first request may relax later checks it would otherwise apply to each request.

    In every case the shape is identical: a per connection decision standing in for a per request decision. The connection state carries a conclusion that was only ever true for the request that produced it.

    How this differs from a length desync

    It is worth separating this cleanly from the classic variants. In a Content-Length versus Transfer-Encoding desync the two servers disagree about byte boundaries, and the exploit hinges on leftover bytes. Here the boundaries can be perfectly clean. Each request is well formed and both servers agree on where it starts and ends. The flaw is that the front end chose not to apply its own checks to requests after the first. That makes connection state request smuggling close in spirit to a request tunnelling attack, where the attacker also reaches the back end on terms the front end did not mean to grant, and it shares a cousin in CL.0 request smuggling, where a front end ignores a body it should have read. All three show up in our injection and input category for the same reason: the front end and the back end hold different ideas about what a connection means.

    Fixing a cached decision

    The defense is to stop reusing a decision that was only valid for one request. Concretely, the front end should route each request by its own Host and path rather than pinning the connection to the first request’s destination. It should re evaluate access rules per request, not per connection. If a connection carries a request for a destination different from the one it was opened for, the safe move is to re route it properly or refuse it, not to assume the first answer still holds. Where trust is established at connection setup, like a client certificate, the front end must still authorize each individual request rather than treating the whole connection as one trusted session.

    This bug is easy to miss because nothing is malformed. Every request is valid, every header is clean, and a scanner looking for a header mismatch finds nothing. What finds it is reasoning about what the front end assumes stays true across a connection, then sending a request that is valid but should not have inherited the first request’s privilege. That is the kind of assumption testing an autonomous researcher is built to do, and you can read how we think about it on our about page.

    Frequently asked questions

    What is connection state request smuggling?

    It is a desync that abuses state a front end keeps about a connection rather than a header parsing mismatch. The front end inspects the first request on a connection, makes a trust or routing decision from it, and applies that decision to every later request on the same connection without checking again. The attacker sends a clean first request to earn the decision, then rides it with requests that should never have qualified.

    How does first request routing cause the problem?

    With first request routing, the front end reads the first request, decides which back end it goes to, and pins the whole connection to that destination. Later requests are never re examined, so their Host and path are ignored. An attacker sends a harmless first request to a public host, then a second request on the same connection aimed at an internal host that the front end forwards without rechecking.

    How is this different from a Content-Length versus Transfer-Encoding desync?

    In a length desync the two servers disagree about byte boundaries and the exploit hinges on leftover bytes. In connection state request smuggling the boundaries are clean and both servers agree where each request starts and ends. The flaw is that the front end chose not to apply its own checks to requests after the first, so the connection carries a conclusion that was only true for the request that produced it.

    How do you prevent connection state request smuggling?

    Stop reusing a decision that was valid for only one request. Route each request by its own Host and path instead of pinning the connection to the first request’s destination, and re evaluate access rules per request rather than per connection. Where trust is set at connection setup, such as a client certificate, still authorize each individual request rather than treating the whole connection as one trusted session.


    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.

  • Request Tunnelling Attack Explained

    Request Tunnelling Attack Explained

    A request tunnelling attack uses a desync for a different goal than classic smuggling. Instead of poisoning a stranger’s request, the attacker opens a straight pipe through the front end to the back end and sends their own second request down it, where front end routing, access rules, and path restrictions never get a chance to look at it. The front end approved one request and, without realizing it, handed over a channel the attacker can keep talking on.

    This builds on the same boundary confusion covered in our primer on http request smuggling, so read that first if the idea of two servers slicing one byte stream differently is new. Tunnelling is the self contained cousin of smuggling, and the difference is worth stating up front.

    What a request tunnelling attack is, and what it is not

    Classic smuggling needs a victim. The attacker leaves a prefix in the back end buffer and someone else’s request completes the exploit. That is both its strength, since the attacker can steal from or poison another user, and its weakness, since the attacker has to win a timing race against real traffic and cannot fully control what lands.

    Tunnelling drops the victim entirely. The second request reaches the back end directly, on the same connection, as the attacker’s own follow on bytes. Nobody else is involved. That makes tunnelling less useful for stealing another user’s data and more useful for one specific job: reaching the back end as if the front end were not there. If the front end is where access control, path filtering, or header stripping lives, a tunnelled request walks in behind all of it.

    Connection reuse is the thing being abused

    To see why a tunnel is even possible, you have to be precise about connection reuse. A front end does not open a fresh connection to the back end for every incoming request. That would be slow. Instead it keeps a pool of open connections and sends many requests, from many users, down each one, one after another. The back end reads that connection as a continuous stream and parses request after request out of it.

    The front end, in the normal case, parses the same stream and inspects each request as it goes by, applying routing and rules to every one. A tunnel exists when the attacker can make the front end stop parsing after the first request while the connection stays open to the back end. Everything the attacker sends after that point is no longer inspected as a request. It is just bytes flowing through an open pipe to a server that will happily parse them into requests.

    Smuggling hides a request inside a request so another user runs it. Tunnelling stops the front end from reading, then speaks to the back end directly on a channel the front end is still holding open.

    Two ways the pipe gets opened

    Abusing a response the front end stops reading

    Some front ends read exactly as many body bytes from the back end as they expect, based on the response headers, and then stop. If the attacker can cause the back end to emit a response the front end under reads, the leftover bytes sit on the connection. The attacker’s next request is then parsed by the back end, not the front end, because the front end already moved on. The connection has quietly become a tunnel: inspected on the first request, raw on everything after.

    Abusing an upgrade

    The cleaner version abuses a protocol upgrade. A client can ask to switch a connection to another protocol with an Upgrade header, for example to a WebSocket or to cleartext HTTP/2 with Upgrade: h2c. If the front end grants the upgrade and then blindly relays the raw connection to the back end, believing it is now carrying an upgraded protocol it no longer needs to inspect, the attacker simply keeps sending ordinary HTTP/1.1 requests. The back end reads them as normal requests, and the front end treats the whole exchange as opaque upgraded traffic. The attacker is now speaking straight to the back end.

    The common thread is that the front end makes a decision, stop reading or hand off this connection, and then stops applying its rules while the back end keeps parsing. That gap is the tunnel.

    What an attacker reaches through the tunnel

    Because the back end trusts requests on an established connection, a tunnel lets the attacker send the requests the front end was built to refuse:

    • Restricted paths. Routes like an internal admin panel or a metadata endpoint that the front end blocks from the outside become reachable, since the tunnelled request never passes the block.
    • Stripped or spoofed headers. Headers the front end adds for trust, or strips for safety, can be set by the attacker directly, because the front end is not rewriting the tunnelled request.
    • Internal only virtual hosts. A Host value the front end would never route to can be sent straight to a back end that still honors it.

    This makes tunnelling a close relative of using a desync to get around a filter, which we cover in request smuggling WAF bypass, and it overlaps with the trust assumptions behind connection state request smuggling, where a front end reuses a decision made on one request for the rest of a connection. All three come from the same root: the front end and the back end do not share one consistent view of what is happening on the connection between them. That root cause runs through our injection and input category.

    Closing the tunnel

    The defenses follow from the mechanism. The front end should not relay a raw connection to the back end after an upgrade unless it is certain the back end actually agreed to that upgrade, and it should refuse upgrade requests for protocols it does not fully broker. It should keep parsing and applying its rules to every request on a connection, not just the first. And where an upgrade or a surprising response could leave the connection in an uncertain state, closing that connection rather than returning it to the pool removes the pipe before anyone can speak through it.

    A tunnel is a subtle bug because every individual request in it is well formed and the front end believes it is behaving correctly. Confirming one takes the same care as any desync: you prove that a request actually reached the back end unfiltered, with a safe and specific piece of evidence, rather than guessing from a header that looked odd. That style of work, reasoning about what each server assumes about the one next to it and then proving the assumption false, is what UnboundCompute is built around, and you can read more on our about page.

    Frequently asked questions

    What is a request tunnelling attack?

    It is a use of a desync where the attacker opens a straight channel through the front end to the back end and sends their own second request down it. The front end stops inspecting the connection after the first request, so the tunnelled request reaches the back end without passing routing, access rules, or path restrictions. It reaches the back end directly rather than poisoning another user.

    How is tunnelling different from classic request smuggling?

    Classic smuggling leaves a prefix in the back end buffer that completes when some other user sends a request, so it needs a victim and a timing race. Tunnelling drops the victim entirely: the attacker’s own follow on bytes reach the back end on the same connection. That makes it weaker for stealing another user’s data but strong for reaching the back end as if the front end were not there.

    How does connection reuse make tunnelling possible?

    A front end keeps a pool of open connections to the back end and sends many requests from many users down each one. The back end parses that stream request after request. A tunnel exists when the attacker makes the front end stop parsing after the first request while the connection stays open, often by abusing an Upgrade header or a response the front end under reads, so later bytes are parsed only by the back end.

    How do you defend against request tunnelling?

    The front end should not relay a raw connection to the back end after an upgrade unless the back end truly agreed to it, and should refuse upgrades for protocols it does not fully broker. It should keep applying its rules to every request on a connection, not just the first. Where an upgrade or an odd response leaves the connection uncertain, closing it instead of returning it to the pool removes the pipe.


    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.

  • Capturing Other Users Requests With a Desync

    Capturing Other Users Requests With a Desync

    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.

  • Request Smuggling WAF Bypass

    Request Smuggling WAF Bypass

    A WAF, a web application firewall, usually sits at the edge of a site and inspects every incoming request, blocking the ones that match known bad shapes before they reach the application. A request smuggling WAF bypass defeats that inspection without defeating any of its rules. The attacker hides a second request inside the body of a first request that the WAF reads as plain data, and the back end later parses that body as a fresh request the filter never saw. This is http request smuggling used to walk a blocked request straight past the one component built to stop it.

    The reason it works is not a weak rule. The WAF can have a perfect list of forbidden paths and payloads. The bypass is that the WAF and the back end disagree about how many requests are even present in the bytes, so the dangerous request does not exist as a request at the moment the WAF looks.

    What the WAF actually inspects

    A WAF reads the request the way the front end frames it. It takes the request line, the headers, and a body whose length it reads from a length header, and it checks that unit against its rules. If the request asks for /admin and the rules block /admin, it is refused. If a header carries an injection string the rules know, it is refused. Inside its own view of the traffic, the WAF is doing its job correctly.

    The trouble is that the WAF treats the body as opaque data. A body is just bytes to forward. So when an attacker sends a request whose body happens to contain the full text of another HTTP request, the WAF sees a normal request with a normal body and waves it through. Nothing in the body is a request as far as the WAF is concerned. It is cargo.

    POST /submit HTTP/1.1
    Host: acme-notes.example
    Content-Length: 4
    Transfer-Encoding: chunked
    
    0
    
    GET /admin HTTP/1.1
    Host: acme-notes.example
    Foo: 

    To the WAF reading Content-Length: 4, the body is four bytes, the chunk terminator, and the request is a harmless POST /submit. The forbidden GET /admin below it is, in the WAF view, just more body bytes. It passes.

    Why the request smuggling WAF bypass works at the back end

    The back end reads the same bytes differently. It honors Transfer-Encoding: chunked, sees the 0 chunk, ends the first request there, and finds the GET /admin lines still sitting in its buffer. It parses them as a brand new request. That request is the forbidden one, and it arrives at the application having never been shown to the WAF as a request. The filter inspected the outer POST /submit and approved it. The inner GET /admin only became a request after it was already past the gate.

    A request smuggling WAF bypass does not break a single rule. It makes the forbidden request invisible at the edge by keeping it disguised as body data until it is safely behind the filter.

    This is why the familiar advice to inspect at the edge fails against a parsing disagreement. Inspection at the edge assumes the edge and the application see the same requests. A WAF can only check what it can parse as a request, and the whole attack is arranging for the dangerous request to not be parseable as one until it has moved past the checkpoint. The control is real and correct. It is simply looking at a different decomposition of the bytes than the back end will use.

    What a bypass gets you

    The point of slipping past the WAF is to reach what the WAF was guarding.

    • Blocked routes and methods. Paths the edge refuses, like administrative endpoints, become reachable because the smuggled request addressing them never passed through the edge as a request.
    • Blocked payloads. An injection string the WAF would catch in a header or parameter rides inside the smuggled request, so the rule that would match it never runs on it.
    • Everything a desync already grants. Once the boundary is broken, the attacker also inherits the ability to poison the shared connection or the cache in front of the app, since the same split that bypassed the filter also mismatches requests and responses.

    The last point matters because a WAF bypass is rarely the final goal. It is a door into the other smuggling outcomes, and the same desynced connection can be driven into response queue poisoning once the filter is behind the attacker.

    Defending when the filter is not enough

    The honest conclusion is that a WAF cannot be the only line here, because the bug is upstream of what the WAF can see. The fixes are the desync fixes, and this is a parsing and input problem of the kind we group under injection and input.

    • Make the front end and WAF own the framing. The edge should rewrite each request into one unambiguous form with a single length it computed, so the back end parses the exact same requests the WAF inspected. If the edge will not honor a Transfer-Encoding, it should strip it rather than forward it.
    • Reject ambiguity instead of guessing. A request carrying both a Content-Length and a Transfer-Encoding should be refused and the connection closed, so no smuggled body can survive as a later request.
    • Normalize before you inspect. A filter that inspects after the request has been reduced to one canonical form is checking the same bytes the back end will run. Inspecting a shape the back end will reparse is the gap the bypass lives in.
    • Prefer HTTP/2 to the back end. End to end framing removes the text length the WAF and back end can read two ways.

    Finding a bypass like this means reasoning about where two components disagree, not matching a signature, which is exactly what a filter cannot do for itself. When UnboundCompute reports a smuggling path through a WAF, it confirms the smuggled request actually reaches an impactful sink before reporting, so a claimed bypass is only a finding once the inner request is shown to hit something the edge was meant to protect. You can read how that proof works on our about page. The WAF was never wrong about its rules. It was wrong about how many requests it was holding.

    Frequently asked questions

    What is a request smuggling WAF bypass?

    It is a way to defeat an edge filter without breaking any of its rules. The attacker hides a forbidden request inside the body of a request the WAF reads as plain data, and the back end later parses that body as a fresh request the filter never inspected as a request.

    Why does inspecting at the edge not stop it?

    Edge inspection assumes the edge and the back end see the same requests. A WAF can only check what it can parse as a request, and the attack keeps the dangerous request disguised as body data until it is past the filter, so there is nothing for a rule to match at the moment of inspection.

    What does a bypass let an attacker reach?

    Blocked routes and methods like administrative endpoints, payloads the WAF would otherwise catch, and every other smuggling outcome. Because the same desync that hid the request also mismatches requests and responses, the attacker can go on to poison the shared connection or a cache in front of the app.

    How do you defend against it?

    Treat the WAF as one layer, not the only one, because the bug is upstream of what it can see. Make the front end own the framing and rewrite each request into one unambiguous form, reject requests carrying both Content-Length and Transfer-Encoding, normalize before inspecting, and prefer end to end HTTP/2.


    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.

  • Request Smuggling Cache Poisoning

    Request Smuggling Cache Poisoning

    A caching layer is supposed to make a site faster by storing one copy of a response and handing it to everyone who asks for the same URL. request smuggling cache poisoning abuses that sharing. The attacker uses a desynced connection to control the request a cache stores, so the cache saves attacker chosen content under a victim URL and then serves it to every visitor who requests that path. This is http request smuggling aimed not at one victim on a connection but at the shared store in front of the application, which turns a single smuggled request into a response that many people receive.

    It helps to separate this from plain web cache poisoning before going further, because they look similar and are not the same bug.

    How request smuggling cache poisoning differs from plain cache poisoning

    Plain web cache poisoning works by finding an input the cache ignores but the application reflects, such as an unkeyed header. The attacker sends a request to the real victim URL, slips a tainted value into that ignored input, and the cache stores the poisoned response under the normal key. The request itself is honest; only a field inside it is abused.

    Request smuggling cache poisoning is different. The attacker does not need an unkeyed input at all. The desync lets them control which request the cache pairs with which URL. In other words, plain cache poisoning bends the content of a legitimate request, while the smuggling version bends the boundary between requests so the cache ends up storing a response for a request the victim never made under a key the victim will later ask for. One abuses a field. The other abuses the parsing disagreement itself.

    Plain cache poisoning lies to the cache about what is in a request. Request smuggling cache poisoning lies to the cache about which request this even is, so the stored copy is bound to the wrong URL from the start.

    Walking the poisoning step by step

    Assume the attacker already has a desync, a CL.TE chain for example, where the front end and back end count a body differently and the front end sits in front of the cache. The attacker smuggles a prefix that will fuse with the next request on the connection. The goal is to make the back end produce a response for an attacker chosen resource while the cache files it under the path the victim will request.

    POST / HTTP/1.1
    Host: acme-notes.example
    Content-Length: 130
    Transfer-Encoding: chunked
    
    0
    
    GET /static/app.js HTTP/1.1
    Host: acme-notes.example
    Foo: 

    The front end honors Content-Length and forwards the whole thing as one request. The back end honors Transfer-Encoding, sees the 0 chunk, ends the first request, and leaves the smuggled GET /static/app.js waiting with a dangling Foo: header. When a victim’s request arrives, its first line is swallowed by the dangling header, and the back end answers the smuggled /static/app.js request. The key part is that the caching layer in the path keys this exchange to the victim’s URL, the one they actually sent, while the body it stores is the response the attacker steered it toward. From then on, visitors asking for that cached path are handed the poisoned copy straight from the store, with no further action from the attacker.

    Flip the resources and the impact changes shape. If the attacker makes a cacheable static path return a redirect to a site they control, every visitor to that path is bounced away. If they make a widely loaded script URL cache a response carrying attacker text, every page that includes that script is affected. The desync chose the request; the cache did the distribution.

    Why the caching layer multiplies the harm

    A desync on its own tends to affect one connection and the users who share it, as in response queue poisoning. Adding a cache changes the scale in three ways.

    • It outlives the connection. A poisoned cache entry sits in the store for as long as its freshness lifetime allows. The attacker can disconnect and the bad copy keeps being served.
    • It reaches everyone, not just the queue. Any visitor who requests the poisoned URL gets the stored copy. The blast radius is every user of that path, not only the ones sharing one back end connection.
    • It needs no repeat. One successful smuggle writes the entry. The cache then does the work of handing it out, which also makes the attack quiet, since no ongoing malicious traffic is required.

    Keeping the cache honest

    The fixes are the desync fixes plus a few that harden the store, and they belong to the same family of parsing and input flaws we group under injection and input.

    • Close the desync first. Request smuggling cache poisoning cannot start without a boundary disagreement, so the primary fix is to stop the desync: reject requests that carry both a Content-Length and a Transfer-Encoding, normalize framing at the front end, and prefer HTTP/2 to the back end.
    • Make the cache key match what the back end answered. If the store keys an entry on a URL, it should be confident the response it stores was produced for that URL. A caching layer that trusts the front end boundary blindly is the component that binds the wrong response to the key.
    • Do not cache ambiguous exchanges. When a request or response looks malformed, or a connection has shown signs of a boundary shift, the safe move is to skip the store rather than persist a copy that might be poisoned.
    • Keep cacheable responses simple. The more a cacheable path reflects request data into its response, the more a smuggled request can steer what gets stored. Static, predictable responses give an attacker less to aim at.

    This class is easy to miss with a scanner because the poisoned entry only appears after a smuggled request and a cache write line up in time, so a one request probe sees nothing wrong. When UnboundCompute chases a desync into a cache, it confirms the smuggled request actually reaches an impactful sink before reporting, which here means proving that a poisoned copy is really stored and served back under the victim URL rather than inferring it from a single odd response. You can read how that verification works on our about page. A cache is a promise that everyone asking for one URL gets the same safe answer. Request smuggling cache poisoning keeps the promise and corrupts the answer.

    Frequently asked questions

    What is request smuggling cache poisoning?

    It is an attack that chains a request smuggling desync with a caching layer. The attacker uses the boundary disagreement to control which request the cache stores against which URL, so attacker chosen content is saved under a victim path and served to every later visitor.

    How is it different from plain web cache poisoning?

    Plain cache poisoning bends the content of a legitimate request, usually through an unkeyed input the cache ignores but the app reflects. The smuggling version bends the boundary between requests, so the cache binds a response for a request the victim never made to a key the victim will later request.

    Why is the caching layer so damaging here?

    A desync alone affects one connection and its shared users. A cache stores the poisoned copy for its freshness lifetime, serves it to every visitor of that URL, and needs no repeat traffic, so a single smuggle reaches far more people and keeps working after the attacker disconnects.

    How do you prevent it?

    Close the underlying desync first by rejecting requests with both Content-Length and Transfer-Encoding and normalizing framing at the front end. Then make the cache confident the stored response was produced for the key it uses, skip caching ambiguous exchanges, and keep cacheable responses simple.


    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.

  • Response Queue Poisoning

    Response Queue Poisoning

    When an attacker desyncs a connection between a front end and a back end, the damage does not stop at one smuggled request. response queue poisoning is the lasting state that follows: once the two servers disagree about where one request ended, the responses coming back fall out of step with the people who asked for them, and every later user on that connection receives an answer meant for someone else. It is one of the harshest outcomes of http request smuggling, because a single well timed request can keep handing strangers each other’s pages until the connection is torn down.

    The reason it lasts is that HTTP on a reused connection is a strict queue. The back end reads requests in order and writes responses in the same order, trusting that the first response belongs to the first request, the second to the second, and so on. Nobody labels a response with whose request it answered. The ordering is the only link. Break the ordering once and nothing on the connection puts it back.

    How the queue shifts by one

    Start from a desynced connection where the attacker has left a smuggled prefix in the back end buffer. That prefix is the beginning of a request that the back end will finish by gluing on whatever bytes arrive next. Now walk the queue one step at a time.

    The attacker sends their smuggled request, which the back end reads as request one and answers with response one. But because of the leftover prefix, the boundaries are now off by one. The back end has produced one more response than the front end expected, or it has merged two requests into one, so the count of responses no longer matches the count of real requests. From this point the mapping is shifted. When the next real user sends their request, the back end hands back a response that was actually generated for the request before theirs.

    victim sends:    GET /account HTTP/1.1
    victim receives: 200 OK  (body: a different user's /account page)

    That is the whole trick in one line. The victim asked for their own account page and received a response the back end built for the request that sat ahead of theirs in the shifted queue. The queue shifting does not undo itself. Each user pulls the response meant for the previous request, so the poisoning rides the connection forward, victim after victim, for as long as the connection stays open and pooled.

    Response queue poisoning is an off by one error in who gets which answer. The requests are fine and the responses are fine. Only the pairing between them is wrong, and nothing on the connection ever repairs it.

    Why response queue poisoning is so damaging

    Consider what a response can contain. On a logged in connection the back end returns pages built for an authenticated session: an account dashboard, an order history, a password reset form with a token already filled in, an API reply carrying a bearer value. When the queue is shifted, those pages land on the wrong browser. The attacker does not need to read a cookie or crack a session. They receive a fully rendered authenticated page that the server addressed to the previous request in line.

    Three properties make this worse than a one shot leak.

    • It is persistent on the connection. One smuggled request poisons the ordering, and every subsequent response on that connection stays shifted until the connection closes. The attacker can keep collecting answers meant for others without sending another attack.
    • It crosses users. The pooled connection carries traffic from many people. The attacker’s request and the victim’s request share the same pipe, so the attacker harvests responses built for strangers, not for themselves.
    • It needs no secret. There is no credential to steal. The server volunteers the private page, correctly rendered, to whoever happens to hold the shifted slot in the queue.

    The same shift can be driven the other direction as well. Instead of waiting to receive a victim’s response, an attacker can store a prefix that captures the victim’s incoming request, a technique we cover in capturing other users requests. Response queue poisoning is the receiving side of that coin: you are not reading what the victim sent, you are reading what the server sent back to them.

    Where the desync usually comes from

    Any desync can start the shift. A length header disagreement in a CL.TE or TE.CL chain is the common source, but a timing split works just as well, which is why a pause based desync attack ends in the same poisoned queue. The queue does not care how the boundary moved. It only cares that the count of responses stopped matching the count of real requests, and once that happens the pairing slides.

    Breaking the queue shift

    The defenses aim at two things: never let a boundary move, and limit the blast radius if one does. This class of flaw sits in how a server parses request boundaries, which is why we file it under injection and input rather than treating it as a content problem.

    • Close the connection on anything ambiguous. A server that sees a request it cannot parse one clear way should respond and then close, so no shifted state survives to the next user. Closing is the step that drains the poison.
    • Normalize framing at the front end. The front end should rewrite every forwarded request into one unambiguous form with a single length it computed, so the back end never counts bytes differently and the queue never shifts in the first place.
    • Limit connection reuse across users. Some deployments stop pooling one back end connection across different users, so even a successful shift cannot deliver one person’s response to another.
    • Prefer HTTP/2 to the back end. End to end HTTP/2 framing marks message boundaries explicitly, so there is no text length for two servers to read two ways.

    Finding this class means reasoning about an ordering that no single request reveals, so a scanner that checks one request at a time tends to miss it. When UnboundCompute investigates a desync, it confirms the smuggled request actually reaches an impactful sink before reporting, which here means proving that a real victim response actually lands on the wrong connection rather than guessing from a timing hint. You can read how that works on our about page. The queue looks orderly from the outside. Response queue poisoning is the proof that order was only ever a convention, held together by a count that an attacker can move by one.

    Frequently asked questions

    What is response queue poisoning?

    It is the lasting state after a connection is desynced, where the back end responses stop matching the requests that asked for them. The pairing shifts by one, so every later user on that pooled connection receives a response that was built for the request ahead of theirs.

    Why does the poisoning persist?

    HTTP on a reused connection is a strict queue with no label tying a response to its request. The order is the only link. Once a smuggled request makes the response count stop matching the request count, nothing on the connection restores the pairing, so it stays shifted until the connection closes.

    Why is it so damaging?

    On a logged in connection the shifted response can be a fully rendered authenticated page, such as an account dashboard or a reply carrying a token. The attacker receives a private page the server addressed to another user, with no cookie to steal and no credential to crack.

    How do you stop it?

    Close the connection after any request a server cannot parse one clear way, normalize framing at the front end so boundaries never move, limit back end connection reuse across users, and prefer end to end HTTP/2 whose framing marks message boundaries explicitly.


    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.

  • Pause Based Desync Attack

    Pause Based Desync Attack

    Most ways to split the boundary between two HTTP servers lean on a header trick, a fight between Content-Length and Transfer-Encoding that makes a front end and a back end count a body differently. A pause based desync attack needs none of that. The attacker sends a request that is deliberately incomplete, then stops sending, and lets the clock do the splitting. This is a timing variant of http request smuggling, and it can work against servers that parse length correctly, because the disagreement it exploits is about when a request ends in time, not how long its declared body is.

    To see why that matters, remember that a front end and a back end share one long lived connection and both slice a continuous byte stream into separate requests. In a classic desync they disagree about a length header. Here they disagree about a pause. One server decides the request is finished at the moment the bytes stop arriving, and the other keeps waiting for more. The gap between those two decisions is where a second request gets smuggled in.

    How a deliberate pause creates the desync

    Every server that reads a request body has a read timeout and some buffering. It will wait only so long for the next byte before it gives up or acts on what it already has. An attacker who controls the timing of the bytes can push the two servers into different states by sending part of a request and then holding still.

    Picture the attacker opening a connection and sending headers that promise a body, then sending the first slice of that body and pausing. If the front end flushes what it has to the back end when its own timer fires, while the back end still believes more of the body is coming, the two servers now hold different ideas about where this request stops. When the attacker finally sends the rest, one server reads it as the tail of the original request and the other reads it as a brand new request on the same connection. That leftover, attacker written chunk becomes a smuggled prefix with no header fight anywhere in sight.

    The early response case, or the pause based desync attack without any length trick

    The sharpest form of this is the style often written as 0.CL, where the back end emits a response before it has read the body at all. Plenty of endpoints do this. A path that always answers with a redirect, or one that rejects the request on a quick check, can return its response the instant it sees the request line and headers, without ever touching the body.

    Consider a request aimed at such a path:

    POST /go HTTP/1.1
    Host: acme-notes.example
    Content-Length: 44
    
    GET /admin HTTP/1.1
    Host: acme-notes.example
    X: y

    Suppose /go always answers with an immediate redirect and never reads the body. The back end sees the request line, decides to reply, sends the redirect, and considers this request done having consumed zero body bytes. The front end, meanwhile, trusts the Content-Length: 44 and still thinks those 44 bytes belong to the request it forwarded. Those 44 bytes are the smuggled GET /admin. They sit in the back end buffer, and the back end now treats them as the first bytes of the next request on the connection.

    The pause is what makes this reliable. The attacker sends the headers, waits for the early redirect to come back, and only then sends the body bytes, so the smuggled request lands cleanly as a fresh request rather than racing the first one. The timing, not a crafted header, is doing the work of choosing a new boundary.

    A pause based desync attack turns a server’s own patience against it. A gap in time, not a header that lies about length, is what decides where one request ends and the next one begins.

    Why this variant is hard to catch

    A length based smuggle leaves a fingerprint. A request carrying both Content-Length and Transfer-Encoding is suspicious on its face, and a careful front end can reject it. A pause based desync attack sends requests that are each well formed. There is no duplicate header to flag, no illegal value, nothing that looks wrong byte by byte. The anomaly lives in the timing of the stream and in one endpoint that answers early, which no request filter is built to notice.

    That is also why testing for it means watching behavior rather than scanning content. A tester sends a request through an intercepting proxy, holds part of the body, and watches whether the connection behaves as if a second request is now queued. The signal is a response that arrives too early or a follow up request that comes back changed. Teams who want the full checklist can read our notes on detecting http request smuggling, since the detection work is the same family regardless of whether a header or a pause caused the split.

    Once the split exists, the consequences are the ones every desync shares. The attacker can poison the shared connection so the next user is served the wrong answer, a problem we cover under response queue poisoning. They can capture a victim’s request into an attacker readable field. They can slip a request past a front end that only ever inspected the harmless looking outer request. The entry road is new, but the town it leads to is familiar.

    Shutting the pause based route

    The fixes rhyme with the rest of the desync family, with one addition aimed at timing. Front ends and back ends in one chain should agree on read timeout behavior, so one of them does not flush a half read request while the other waits. An endpoint that can answer before reading the body should still drain or discard that body, or the server should close the connection after an early response so no leftover bytes can wait for a victim. End to end HTTP/2 helps here too, because its framing marks message boundaries explicitly and leaves no room for a pause to be read two ways. More broadly, this is an input handling flaw, the kind we group under injection and input, where the bug is in how a boundary is parsed rather than in any single value.

    What makes this class worth real attention is that it hides inside normal looking traffic, so finding it means reasoning about what each server assumes about timing rather than matching a payload. When UnboundCompute probes for a desync like this, it confirms the smuggled request actually reaches an impactful sink before reporting, so a timing quirk is only called a finding once it is shown to move a real request somewhere it should never go. You can read more about that approach on our about page. The attack is quiet by design, and the defense is to stop assuming two servers will ever agree on when silence means a request is done.

    Frequently asked questions

    What is a pause based desync attack?

    It is a timing variant of request smuggling where the attacker sends an incomplete request and then pauses. A server read timeout or an early response makes one server decide the request is finished while the other waits for more, so the leftover bytes become a smuggled request on the shared connection.

    How is it different from a CL.TE or TE.CL smuggle?

    Those variants rely on a length header disagreement, so a request carries both Content-Length and Transfer-Encoding and the two servers count the body differently. A pause based desync needs no conflicting header. The split comes from the timing of the bytes and from an endpoint that answers before reading the body.

    What is the 0.CL style?

    It is the case where a back end sends a response before reading any body, so it treats the request as having zero body bytes while the front end still trusts the declared Content-Length. The declared body bytes, written by the attacker, then sit in the buffer and are parsed as the start of the next request.

    How do you defend against it?

    Align read timeout behavior across the front end and back end, drain or discard the body on endpoints that answer early, and close the connection after an early response so no leftover bytes can wait for a victim. End to end HTTP/2 also removes the ambiguity because its framing marks message boundaries explicitly.


    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.

  • CL.0 Request Smuggling

    CL.0 Request Smuggling

    CL.0 request smuggling is the variant where the back end treats the body of a request as if it had length zero, no matter what the Content-Length header says. The front end reads Content-Length honestly and forwards the body, but the back end endpoint on the other side ignores it, so those forwarded body bytes are left over and read as the next request on the connection. For the broader mechanics of how two servers disagree about where a request ends, start with our primer on http request smuggling.

    Why the zero in CL.0 request smuggling

    The smuggling variants are named by how each server reads length, front end first and back end second. CL.TE means the front end trusts Content-Length and the back end trusts Transfer-Encoding. CL.0 follows the same scheme, but the back end does not trust another header, it simply treats the body length as zero. The front end counts the body and sends it. The back end decides there is no body at all. Everything the front end forwarded as the body is now, from the back end’s point of view, the beginning of a brand new request.

    CL.0 is not two parsers reading two different headers. It is one server that forwards a body and another that was never going to read it, so the body becomes a request by default.

    Which endpoints ignore the body

    Plenty of request handlers have no reason to expect a body and are written as if one can never arrive. Those are the CL.0 candidates:

    • Static file handlers. A server returning an image or a stylesheet reads the path and streams the file. It does not read a request body, so a body you attach is ignored and stranded.
    • Redirects. A handler whose whole job is to answer with a Location header often replies immediately and never touches the body.
    • Error and default pages. A 404 or a catch all handler that fires before any routing may answer without consuming what you sent.
    • Server level responses. Health checks, redirects to HTTPS, and other responses produced by the web server itself rather than the application behind it frequently treat every request as bodyless.

    The common thread is a handler that commits to a response based only on the request line and headers, then returns without draining the body the Content-Length promised.

    Why CL.0 request smuggling is per endpoint, not per site

    This is the part that makes CL.0 slippery. A classic CL.TE desync is a property of the two servers and their parsers, so it tends to hold across the whole site: if the pair disagrees on one path, it disagrees on most of them. CL.0 is decided by one handler. The same back end can read Content-Length correctly on /api/login, consume the body, and be perfectly safe, while the handler for /static/logo.png on the same server ignores the body and desyncs. One path is exploitable and the path next to it is not. That is why you cannot test a site once and call it clean. You have to find the specific endpoint whose handler throws the body away.

    Finding a CL.0 endpoint

    Hunting for CL.0 is endpoint by endpoint work. You make a list of paths that are likely to answer without reading a body, static assets, redirects, error pages, and server level routes, then test each one for the desync signature. The test is a single connection carrying two requests. You send a POST to the suspect path with a Content-Length that covers a smuggled request hidden in the body, then send a second, ordinary request on the same connection, and watch which response comes back:

    POST /static/logo.png HTTP/1.1
    Host: acme-notes.example
    Content-Length: 34
    
    GET /admin HTTP/1.1
    Foo: bar

    If the endpoint honors Content-Length, it reads the 34 byte body, the smuggled lines never become a request, and your second request gets its own normal response. If the endpoint ignores the body, the smuggled GET /admin is left in the connection, your second request is appended to it, and the response you get back answers the smuggled request instead. That difference in the second response is the signal that the handler is a CL.0 endpoint. Working through the candidate paths this way is covered in more depth in detecting http request smuggling, and it belongs to the family of injection and input flaws, where bytes meant as data get read as structure.

    Shutting it down

    The fix lives at the endpoints that ignore bodies. A handler that does not expect a body should either read and discard the whole declared body before responding, or refuse requests that carry one and close the connection so nothing is left stranded. The front end can help by normalizing requests and by not forwarding a body to a route it knows is bodyless, but the durable fix is making every handler account for the Content-Length it was sent, even when it has no use for the body.

    CL.0 is a good lesson in how narrow a smuggling bug can be. A site can pass every test on its application routes and still hand an attacker one static path that desyncs, because the flaw is a single handler forgetting a body, not a site wide parser mismatch. Finding it means checking endpoints one by one and asking what each handler assumes about the request it answers. That is the kind of per endpoint reasoning an autonomous researcher is built to carry out, and at UnboundCompute we verify a desync is actually reachable before reporting it, so a reported CL.0 endpoint comes with a proven request sequence rather than a hunch.

    Frequently asked questions

    What is CL.0 request smuggling?

    It is the variant where the back end treats a request body as length zero regardless of the Content-Length header. The front end reads Content-Length and forwards the body, but the back end endpoint ignores it, so the forwarded body bytes are left over and read as the start of the next request on the connection.

    Which endpoints are usually vulnerable?

    Handlers that have no reason to expect a body: static file handlers, redirects, error and default pages, and responses produced by the web server itself such as health checks. They commit to a response from the request line and headers alone, then return without draining the body that Content-Length promised.

    Why is CL.0 per endpoint rather than per site?

    A classic CL.TE desync is a property of the two servers’ parsers, so it tends to hold across the whole site. CL.0 is decided by one handler. The same back end can read Content-Length correctly on an application route and ignore it on a static path, so one endpoint is exploitable while the one next to it is safe. You have to find the specific handler that throws the body away.

    How do you find a CL.0 endpoint?

    List paths likely to answer without reading a body, then test each on a single connection. Send a POST with a Content-Length that covers a smuggled request in the body, follow it with an ordinary request, and watch the second response. If it answers the smuggled request, the handler ignored the body and the endpoint is CL.0.


    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.

  • Browser Powered Request Smuggling

    Browser Powered Request Smuggling

    For years request smuggling was something you ran from your own machine with an intercepting proxy, aimed at a back end that pooled connections between strangers. The technique called browser powered request smuggling flips the sender: the victim’s own browser issues the crafted request, so the smuggled bytes travel on the victim’s connection with the victim’s cookies attached. If the underlying desync is unfamiliar, read the primer on http request smuggling first, then treat this post as the delivery half.

    Why browser powered request smuggling hits the victim directly

    The old model had a limit. Running the attack from your own client meant the poisoned connection was between you and the server, so you could smuggle a prefix onto a shared back end socket and hope it landed in front of some other user’s request. You never knew whose. Driving the attack from the victim’s browser removes the guessing. The connection belongs to the victim, the cookies are the victim’s, and the request you prefix is the victim’s own next request. You are not fishing on a shared pool. You are steering one person.

    Moving the sender into the victim’s browser changes smuggling from an attack on whoever happens to share a connection into an attack on one chosen person, because the poisoned connection is the one their browser already holds.

    What the browser will and will not let you send

    A browser is not an intercepting proxy. It guards request structure on purpose. JavaScript cannot put a raw carriage return and line feed into a header, cannot set forbidden headers like Host or Content-Length to arbitrary values, and cannot open a raw socket and type bytes. What it can do is set the request body to anything at all. So browser powered smuggling only works when the desync can be triggered from the body alone, with no forged header structure required. That is exactly the shape a client side desync gives you: an endpoint that detaches the body, so a request line you placed inside the body becomes a prefix without any header trickery.

    The sending is done with fetch, and a few of its options are what make the connection behave the way the attack needs:

    • credentials: "include" so the victim’s cookies ride along and the smuggled request is authenticated as them.
    • mode: "no-cors" so the cross origin request is allowed to go out even though the attacker’s page cannot read the response. Reading it is not the point; poisoning the connection is.
    • redirect: "manual" so the browser does not chase a redirect and tear the connection down before the second request lands on it.
    • a POST method with a text body that carries the smuggled request line and its headers, which is the prefix you want stranded in the socket.

    The two step sequence

    The attack is two requests on one connection. The attacker page issues the first fetch to the vulnerable endpoint. That endpoint detaches the body, so the smuggled prefix is left sitting in the connection. Then the page issues a second request to the same origin, or navigates the victim there, and the browser reuses the poisoned connection. The server reads the stranded prefix, then the second request, and runs the attacker’s request carrying the victim’s session. Because the first response is one the attacker does not even need to read, no-cors is no obstacle. Everything hangs on the second request landing on the same socket as the first.

    An example pouring a smuggled GET into the body of a request to a body detaching endpoint looks like this from the attacker’s page:

    fetch("https://acme-notes.example/redirect", {
      method: "POST",
      credentials: "include",
      mode: "no-cors",
      redirect: "manual",
      body: "GET /account HTTP/1.1\r\nFoo: bar"
    });

    The body is plain text to the browser. On the wire it is the request body the vulnerable endpoint forgets to read, and those bytes become the start of the next request the server parses on that connection.

    Where it fits and how to contain it

    This is a delivery technique, not a new desync. It rides on top of a connection that can be poisoned from the body, which in practice means a client side desync, and we cover that underlying bug in client side desync attack. Read together, one post is the flaw and this one is how an attacker reaches a real victim with it. It belongs to the broader family of injection and input issues, where attacker controlled bytes cross into a place that reads them as structure.

    Containment is mostly about the server, not the browser, because the browser is only the courier. Fix the endpoint that detaches bodies so no prefix can be stranded, close connections that answer early, and the courier has nothing to carry. On the browser side, the same origin and connection reuse behavior the attack rides on is standard and not something you want to break, so the durable fix is at the server that desyncs.

    Browser powered delivery is a reminder that a server bug and its reach are two different questions. A desync that looked low risk because it needed a shared back end can become a direct attack on a named victim once the browser is the sender. Judging that reach takes more than spotting the quirk; it takes proving the second request really lands on the poisoned connection. That is the kind of end to end proof an autonomous researcher is built to produce, and at UnboundCompute we verify a desync is actually reachable before reporting it, so what you get is a confirmed path rather than a theory.

    Frequently asked questions

    What is browser powered request smuggling?

    It is a delivery technique where the victim’s own browser, driven by JavaScript on an attacker page, sends the crafted request instead of the attacker’s own tool. Because the browser sends it, the smuggled bytes travel on the victim’s connection with the victim’s cookies, which turns a server side desync into a direct attack on one chosen person.

    What fetch settings does it rely on?

    The attacker page uses fetch with credentials: "include" so the victim’s cookies ride along, mode: "no-cors" so the cross origin request is allowed to leave even though the response cannot be read, and redirect: "manual" so the browser does not follow a redirect and tear down the connection. The request is a POST whose body carries the smuggled request line.

    Why must the smuggle work from the body alone?

    A browser guards request structure. JavaScript cannot put a raw carriage return and line feed into a header, cannot set forbidden headers to arbitrary values, and cannot open a raw socket. It can set the body to anything. So browser powered smuggling only works on a desync that can be triggered from the body, which is exactly what a client side desync endpoint provides.

    How do you contain it?

    Fix the server, not the browser. The browser is only the courier, so the durable fix is the endpoint that detaches bodies: read or discard the whole declared body before responding, and close connections that answer early. Once no prefix can be stranded, the browser has nothing to carry.


    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.