Web Cache Deception in 2026: The Path-Confusion Variants That Still Work
Most of the parser-differential work on this blog has been about two servers on one connection disagreeing over where a request ends. Request smuggling is the canonical case. But the same underlying bug, two parties parsing one string differently, shows up anywhere two systems interpret a URL and only one of them makes a security decision from it. The clearest non-smuggling example is web cache deception, and despite being nine years old it is still live on real infrastructure in 2026, for reasons that are structural rather than incidental.
The original trick, restated as a parser differential
Omer Gil’s 2017 “Web Cache Deception” attack is usually explained as a caching bug. It is more precisely a disagreement between two parsers. The victim is sent a URL like:
https://site.example/account/settings/nonexistent.css
The origin routes this to the account/settings handler and ignores the trailing /nonexistent.css, so it returns the victim’s authenticated account page. The cache in front of the origin makes a different decision: it sees a path ending in .css, concludes the response is a static asset, and stores it. The attacker then requests the same URL and is served the victim’s cached account page.
Nothing here is a caching bug in isolation, and nothing is a routing bug in isolation. The vulnerability is the delta between the origin’s parser (which treats the path as account/settings plus ignored trailing junk) and the cache’s parser (which treats the same path as “a thing ending in .css”). That is the identical shape as a CL/TE desync, moved from the connection layer to the caching layer.
Why it survived nine years
The 2020 USENIX study “Cached and Confused” measured this class in the wild and found it widespread; the surprising part in 2026 is how little changed. The reason is that the two decisions genuinely live in two different systems with two different design goals, and neither one is obviously wrong on its own:
- The cache wants to be cheap and fast, so it decides cacheability with a heuristic on the path: static-looking extensions, or a static path prefix. It usually does not consult the origin’s routing logic, because that would defeat the point of caching.
- The origin framework wants to be forgiving, so it routes on the meaningful part of the path and tolerates trailing segments, path parameters, and encoded characters that the developer never thinks about as security-relevant.
Fixing it requires the two systems to agree on how the path is parsed, and they are built by different vendors, deployed by different teams, and tuned for different things. That is exactly why request smuggling persisted too. Agreement across a trust boundary is the hard part; each side in isolation looks fine.
The 2026 variants worth testing
The plain .css suffix is patched on the obvious targets now, usually by the cache being taught not to cache authenticated responses regardless of extension. What still works are the variants that exploit a finer-grained parsing disagreement, where the cache and origin split the path at a different point:
- Delimiter discrepancies. The origin treats
;, or a path parameter, or an encoded%3Fas ending the meaningful path; the cache does not, or vice versa.GET /account/settings;x.csscan route toaccount/settingsat the origin while the cache keys and caches the whole string as static. - Encoded-slash disagreement.
%2Fis where a lot of these live. If the cache decodes%2Fbefore pattern-matching and the origin does not, or the reverse, the two see different path structures for the same bytes. - Newline and null truncation in the cache key. A cache that truncates its key at
%0Aor%00while the origin serves the full path lets an attacker cache a sensitive response under a key the victim’s own requests will also hit.
Every one of these is a controlled experiment: send the same crafted path through the stack, capture what the origin returns and whether the cache stored it, and vary one delimiter at a time. The methodology is the one from the smuggling work, just with “did the cache store an authenticated response” as the oracle instead of “did the next request get poisoned.”
Detection and the actual fix
The robust fix is the same principle that eventually tamed smuggling: stop inferring, start agreeing. Concretely, the cache should cache a response only when the origin explicitly authorises it through Cache-Control, rather than guessing from the URL’s extension. An origin that returns Cache-Control: no-store on every authenticated response closes the whole class regardless of how the path is parsed, because the cache no longer has a decision to get wrong. Where extension-based caching is unavoidable, the cache and origin must share one normalisation library so that %2F, path parameters, and trailing segments resolve identically on both sides before either makes a decision.
On the defensive-monitoring side, the signal is a cache storing a response that carries a Set-Cookie or an authenticated body, keyed under a static-looking path. That combination should never happen and is cheap to alert on.
Web cache deception is not a clever edge case that will be designed away. It is the caching layer’s version of the same disagreement that request smuggling exploits at the connection layer, and it will keep working for exactly as long as caches and origins are allowed to parse the same URL differently.