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-Lengthand aTransfer-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.
