August 10, 2026
Web Cache Deception vs Web Cache Poisoning: The Attack Paths Most Hunters Confuse
Two bugs. Same word — “cache” — in both names. Completely different kill chains. And I’ve seen enough duplicate reports and “N/A, not a…

By CYBER MIND SPACE
4 min read
Two bugs. Same word — "cache" — in both names. Completely different kill chains. And I've seen enough duplicate reports and "N/A, not a real vuln" triage responses to know most hunters still mix these up.
Let's fix that in one read.
The Core Difference (Say This Out Loud)
Cache Poisoning = You control what gets stored in the cache, and everyone else who requests that URL gets your malicious response.
Cache Deception = You trick the cache into storing someone else's private response, and then you go fetch it.
One is "I poison the well, everyone drinks." The other is "I trick the well into holding your water, then I drink it."
Different victim. Different mechanism. Different POC. Stop treating them as the same finding.
Web Cache Poisoning — The Attack Path
The whole game is cache key manipulation. Caches don't store the full request — they store a response keyed on a subset of the request (usually just the URL path, sometimes a few headers). Anything not in the cache key is called an unkeyed input. If the origin server's response changes based on an unkeyed input, but the cache doesn't know that input exists, you've got a poisoning primitive.
Classic hunting checklist:
- X-Forwarded-Host / X-Forwarded-Scheme abuse — inject a header the cache ignores but the app reflects (in a canonical link tag, a password reset link, a JS asset path). Cache stores your poisoned response under the legit URL. Every subsequent visitor gets your payload.
- Fat GET requests — some apps process the body on a GET even though it's not supposed to have one. Body isn't in the cache key. Free injection point.
- Parameter cloaking —
?utm_content=1;param=payload— depending on parser inconsistency between the cache and the origin, this can smuggle a second parameter that only the backend sees. - Cache probing with Param Miner — this is non-negotiable in your toolkit. It brute-forces headers and params to find which ones are unkeyed but still influence the response. If you're not running this on every cached endpoint, you're leaving P1s on the table.
- Fragmentation attacks / CPDoS (Cache Poisoning Denial of Service) — send a request that causes the origin to throw an error page (oversized header, HTTP Header Oversize, HHO), get that error page cached instead of the real page. Now it's a mass DoS, not just a defacement.
Impact ceiling: stored XSS served to every visitor, open redirect at scale, JS supply-chain style takeover if you can poison a cached JS file, or a full site-wide DoS. This is why poisoning bugs consistently land P1 — one payload, infinite blast radius, zero interaction needed from the victim.
Web Cache Deception — The Attack Path
This one's about path confusion between the cache layer and the application.
The setup: a cache is configured to store static-looking resources — anything ending in .css, .js, .jpg, .json — because "static files are safe to cache, right?" The cache makes that decision by looking at the URL, not the actual content-type of the response.
Now the trick: you request /account/settings/nonexistent.css.
The backend framework — Rails, Django, Express, whatever routing engine — sees /account/settings/ as the real route, treats nonexistent.css as a path parameter or just 404s softly while still rendering the authenticated page content (this depends heavily on framework routing behavior — some frameworks strip trailing segments, some do soft-404 with 200 status). The origin serves the victim's private account page — API keys, CSRF tokens, PII, whatever's on that dashboard — but because the URL ends in .css, the cache says "static asset, cache it" and stores that authenticated response under a public, guessable-if-shared cache key.
Attack path in practice:
- Send victim a link: https://target.com/account/api-keys/style.css
- Their authenticated session hits it, sensitive page gets rendered, cache stores it because of the extension.
- You request the exact same URL — no auth needed — and the cache serves you the victim's cached, authenticated response.
James Kettle's original research and the follow-up work by Omer Gil (the guy who really weaponized this class) both nail the same point: this bug lives in the mismatch between how the cache classifies "cacheable" and how the origin actually handles routing. Path normalization differences, static-extension allowlisting, and delimiter confusion (.css, ;, %2e, #) are your primary attack surface.
Where to hunt for it:
- Any endpoint serving personalized/authenticated data
- CDN/reverse proxy configs that cache by extension pattern (super common in AWS CloudFront + Nginx
location ~* \.(css|js|jpg)$setups) - Frameworks known for loose route matching — test trailing garbage extensions on every authenticated route, not just the obvious ones
Impact ceiling: account takeover via leaked session tokens, PII disclosure, CSRF token theft leading to chained account takeover. It's victim-specific, not mass — but per-victim impact can be brutal.
The One-Line Distinction You Repeat in Your Report
"Cache Poisoning is a write primitive on shared cache state. Cache Deception is a read primitive exploiting cache/origin classification mismatch."
Put that line in your triage notes. Programs that reject Deception reports as "just a caching misconfig" usually change their tune once you demonstrate you pulled a real victim's session data with zero interaction beyond a clicked link.
Defenses — What You Should Actually Recommend
For Poisoning:
- Include every input that affects the response in the cache key, or strip/normalize those headers at the edge before they reach origin
- Disable caching on any endpoint where headers influence rendered output unless you've explicitly keyed on them
- Validate
Hostheader server-side, don't trust it for URL generation - Rate-limit and monitor for CPDoS patterns — oversized headers, malformed requests hitting cached routes
For Deception:
- Never let cache eligibility be decided by URL extension alone — check the actual
Content-Typeand cache-control headers the origin sets - Origin should return a hard 404 for genuinely non-existent static paths, not a soft-200 with page content
- Set
Cache-Control: private, no-storeexplicitly on every authenticated route — don't rely on defaults - Normalize path parsing between CDN/proxy and origin so there's no delimiter confusion
Bottom Line for Hunters
Both bug classes reward the same mindset: stop assuming the cache and the origin see the URL the same way. That gap — normalization, key composition, extension trust — is where both these bug classes are born. Test every cached endpoint with Param Miner for poisoning, and test every authenticated route with fake static extensions for deception. Most programs haven't fixed either properly because most triagers still don't understand the difference between them.
Now you do. Go find the gap.