Web Cache Poisoning & Deception
Weaponizing the Cache
Caching exists to serve responses faster, but a cache is shared among users. If an attacker can make a cache store a malicious response, that response is then served to everyone who requests the same key. Web cache poisoning turns a performance feature into a mass-distribution vulnerability.
Cache Keys and Unkeyed Inputs
A cache stores a response under a cache key, typically the URL plus a few headers. Inputs that influence the response but are not part of the cache key are called unkeyed inputs. If the application reflects an unkeyed input into the response, an attacker can inject content that gets cached and served to other users.
Common unkeyed inputs include headers such as:
X-Forwarded-HostandX-Forwarded-SchemeX-HostandX-Forwarded-ServerX-Original-URLandX-Rewrite-URLForwarded
If a cached page reflects X-Forwarded-Host into an absolute URL (say in a <link> or a redirect), an attacker who sends X-Forwarded-Host: evil.example can poison the cache with a response that points every future visitor to the attacker's domain.
Cache Deception
Web cache deception is the inverse: it tricks the cache into storing a victim's private, authenticated response as if it were a public static asset. An attacker appends a static-looking extension to a sensitive URL—for example, /account becomes /account.css—and lures the authenticated victim to visit it. If the cache decides based on the extension that this is a static, cacheable resource, it stores the victim's personal page. The attacker then requests the same URL and reads the victim's data from the cache.
Escalation to XSS and Takeover
A poisoned cache is a stored-XSS delivery mechanism. If the reflected unkeyed input flows into an HTML or JavaScript context, the attacker can cache arbitrary script that executes for every visitor of that URL. Because the payload is served by the trusted origin, it bypasses many client-side defenses. Poisoned responses can also enable redirects to phishing pages or complete domain/asset takeover when the injected value controls an included resource.
Finding It
Testers send a request with a unique marker in each header, examine whether the marker appears in the response, and then check whether that response is cached (via Age, X-Cache, or a follow-up request with the header removed). Tools like Burp's "Param Miner" automate the hunt for unkeyed inputs and cacheable reflections. The careful part is verifying the poison actually hits the cache without disrupting real users.
Defenses
- Include relevant inputs in the cache key, or exclude unknown/irrelevant headers from being reflected at all.
- Do not reflect request headers into responses unless they are strictly validated and normalized.
- Normalize and reject unexpected headers at the edge; strip
X-Forwarded-*from untrusted clients. - Cache only truly static assets, never authenticated or personalized responses; mark private responses
Cache-Control: private, no-store. - Make cacheability explicit rather than relying on default heuristics based on file extension.
- Separate the cache by authentication state, and never serve an authenticated response from a shared cache.
The core lesson mirrors request smuggling: whenever a shared layer and an origin disagree about the meaning of a request or response, an attacker can exploit the gap to affect everyone behind that layer.