Web cache poisoning tricks a caching layer (CDN, proxy, app cache) into storing a malicious response under a normal URL and serving it to every user. It works via unkeyed inputs - something the app reflects into its response that the cache doesn't include in its cache key (often a header). The attacker crafts that input, the poisoned response is cached under the normal key, and everyone gets it until expiry. Its cousin, cache deception, does the opposite: tricks the cache into storing a victim's personalised page, then steals it. The fix: align the cache key with everything that influences the response - don't reflect untrusted headers, key on (or don't cache) header-varying/personalised responses. Detail below; pairs with request smuggling.
// 01 The danger of a shared cache
Caches make the web fast by storing a response once and serving it to many users - a CDN, a reverse proxy, or the app's own cache sits in front and hands out saved copies. That efficiency is also the attack surface. Web cache poisoning is when an attacker manipulates the caching layer into storing a malicious response and serving it to other users. Unlike a typical web attack that compromises one victim, a poisoned cache entry compromises everyone who requests that page until it expires - one request, mass impact. That amplification is why cache poisoning, like request smuggling, is rated high when confirmed: the blast radius is the whole user base, and it operates at the infrastructure layer most testers overlook.
// 02 Cache keys & unkeyed inputs - the core theory
To understand the attack you need one concept: the cache key. A cache decides whether two requests are “the same” - and can share a stored response - by comparing a subset of the request: typically the path and query string, sometimes a few headers. Everything not in that key is unkeyed: the cache treats two requests that differ only in an unkeyed input as identical, and serves the same cached response for both. Now the flaw: if the application changes its response based on an unkeyed input - say a header the cache ignores - an attacker can send a request with a malicious value for that unkeyed input. The app produces a poisoned response; the cache stores it under the normal key; and the next user - whose request matches that key but doesn't include the malicious header - is served the attacker's poisoned content. The whole attack lives in the gap between what the app reads and what the cache keys on.
// 03 What poisoning delivers
Mass XSS
Poison a response with injected script - executed in every subsequent visitor's browser.
Malicious redirects
Redirect all users to an attacker site - phishing or malware at scale.
Resource hijack
Poison a cached script/style URL so every page loading it runs attacker code.
Denial of service
Cache an error or broken response under a key, breaking the page for everyone.
The severity tracks the reach: a reflected XSS that normally hits one victim becomes a stored, cache-delivered XSS hitting all users - the same payload, vastly amplified by the cache.
// 04 Cache poisoning vs cache deception
There's a mirror-image attack worth knowing: web cache deception. Poisoning pushes malicious content out to many users. Deception pulls sensitive content out of one user. In deception, the attacker crafts a URL that the victim visits (or is lured to) which the cache mistakenly treats as a static, cacheable resource - so the victim's personalised, authenticated response (containing their account details) gets stored in the shared cache. The attacker then requests the same URL and retrieves the victim's cached private data. Both attacks stem from the same root cause: a mismatch between how the application and the cache interpret a request. And both are tested in a thorough web assessment - because the caching layer is a security boundary, not just a performance one.
// 05 How to test & defend
Testing is methodical: identify the caching layers, then hunt unkeyed inputs - send requests with unusual or crafted headers (and other inputs the cache may ignore) and observe whether they're reflected into the response and whether that response then gets cached and served to a clean request. Confirmation must be careful, since poisoning a live cache affects real users, so it's controlled and scoped in the rules of engagement. Defending means closing the app/cache gap: don't reflect untrusted inputs like arbitrary headers into responses; if the app does vary on a header, make the cache key on it or not cache those responses; disable caching for header-dependent or personalised responses (this also kills cache deception); and configure the CDN/proxy so its caching rules match the application's actual behaviour. The flaw lives between the app and the cache, so the fix is reviewing them together - part of every web app test, per our methodology.
// 06 Frequently asked questions
What is web cache poisoning?
An attack manipulating a caching layer (CDN, proxy or app cache) into storing a malicious response and serving it to other users. It works when the app reflects an input into its response that the cache doesn't include in its cache key (an unkeyed input). The attacker crafts that input, the poisoned response is cached under the normal key, and every subsequent user gets it until expiry.
What is a cache key and why does it matter?
The set of request components a cache uses to decide whether two requests can share a response - typically path and query string, sometimes selected headers. Anything not in the key is unkeyed: the cache treats requests differing only in an unkeyed input as identical. Poisoning exploits this - if the app behaves differently on an unkeyed input, an attacker can influence the cached response others receive.
Poisoning vs cache deception?
Opposite directions. Poisoning: the attacker poisons a cached response so other users get malicious content. Deception: the attacker tricks the cache into storing a victim's sensitive, personalised response, then retrieves it - stealing their data. Poisoning pushes malicious content to many; deception pulls sensitive content from one. Both stem from app/cache interpretation mismatches.
How do you prevent it?
Align the cache key with everything that influences the response, so no output-changing input is unkeyed. Don't reflect untrusted headers into responses; if the app varies on a header, key on it or don't cache those responses; disable caching for header-dependent or personalised responses; and configure the CDN/proxy so its rules match the app's behaviour. The flaw lives in the gap between app and cache.
// 07 Related reading
- HTTP request smuggling — another infrastructure-layer, mass-impact attack.
- CORS misconfiguration and OWASP Top 10 in practice.
- Web application penetration testing and our methodology.