Web cache poisoning turns a performance feature into an attack multiplier: one malicious request, cached once, gets served to every visitor who hits that cache key until it expires or is purged. The technique isn't new — James Kettle of PortSwigger formalized modern exploitation methods in "Practical Web Cache Poisoning: Redefining 'Unexploitable'" at Black Hat USA 2018, demonstrating stored XSS against Red Hat's homepage and Mozilla's Firefox Shield infrastructure by abusing the X-Forwarded-Host header. Two years later, his follow-up research "Web Cache Entanglement" (Black Hat USA 2020) showed the problem gets worse in multi-layer cache architectures, where confusion between cache keys at different tiers lets an attacker poison a response indirectly. The problem hasn't gone away: a 2024 academic study presented at ACM CCS, "Detecting and Measuring Web Cache Poisoning in the Wild," scanned the Tranco Top 1000 domains and found roughly 17% — more than 1,000 endpoints across 172 domains — vulnerable to some variant of the attack. This post breaks down the core mechanism, the specific headers attackers abuse, a related but distinct bug class called web cache deception, and the concrete CDN and reverse-proxy configuration changes that close the gap.
What actually makes a cache poisonable?
A cache becomes poisonable when there's a mismatch between its cache key — the subset of a request it uses to decide whether to serve a stored response — and the full set of inputs the origin server actually uses to generate that response. Most caches key on the URL path plus a handful of headers like Host and Accept-Encoding, but backend application code frequently reads other headers to build the response: reflecting X-Forwarded-Host into a canonical link tag, honoring X-Forwarded-Proto to decide whether to redirect to HTTPS, or trusting X-Original-URL for internal routing. Any header, cookie, or parameter the origin reads but the cache ignores is called an "unkeyed input." An attacker sends one request with a malicious value in that unkeyed input, the origin builds a response containing the injected value, and the cache stores that poisoned response under the ordinary, unsuspicious-looking cache key — so every subsequent visitor requesting that same URL gets served the attacker's payload without sending a malicious request themselves.
Which specific headers get abused, and how?
The headers most commonly implicated are X-Forwarded-Host, X-Forwarded-Scheme, X-Original-URL, and X-HTTP-Method-Override, because they were designed to let a reverse proxy tell the origin about the original client request, and many frameworks trust them implicitly. In Kettle's Red Hat demonstration, the application reflected X-Forwarded-Host into an Open Graph URL inside a meta tag on the homepage without validation; setting that header to a payload that broke out of the tag's attribute caused the origin to emit an injected <script> element, and the cache stored that poisoned page for every subsequent visitor. X-Forwarded-Scheme and similar proto-override headers have caused cache-served redirect loops and mixed-content injection when applications use them to build absolute URLs. X-Original-URL and X-Rewrite-URL are especially dangerous because some frameworks (notably certain Symfony and Zend/Laminas configurations) use them to override routing entirely, meaning an attacker can cause the origin to serve a completely different internal endpoint than the URL the cache thinks it's storing.
How is web cache deception different from cache poisoning?
Web cache deception, documented by researcher Omer Gil in 2017, inverts the poisoning attack: instead of tricking the cache into storing a malicious response for everyone, the attacker tricks it into storing someone else's private response under a URL the attacker controls and can later request. It exploits cache rules that key on file extension — a common CDN default caches anything ending in .css, .jpg, or similar static-looking paths, assuming those paths are always static and non-sensitive. If an application ignores path suffixes after the recognized route (serving /account/settings regardless of what's appended), a victim visiting /account/settings/nonexistent.css gets their authenticated account page cached under that exact URL because the extension matched a static-caching rule. The attacker then requests the identical URL and receives the victim's cached, authenticated response — including session data or personal information — with no header manipulation required at all, just a crafted path shared via a link.
What did Cloudflare's own research find about mitigating this at the edge?
Cloudflare published a dedicated post, "How Cloudflare protects customers from cache poisoning," documenting edge-level defenses built directly into its CDN rather than relying solely on origin-side fixes. The core approach it describes is normalizing and validating unkeyed headers before they reach cache logic, combined with cache-key configuration that lets customers explicitly declare which headers, cookies, and query parameters a given route's response actually depends on — closing the keyed/unkeyed mismatch that makes poisoning possible in the first place. This reflects a broader industry shift: rather than trusting origin applications to sanitize every forwarded header correctly, CDN vendors increasingly treat cache-key completeness as an edge-layer configuration problem, exposed through products like Cloudflare Cache Rules, Fastly's VCL, and CloudFront cache policies, so security teams can enforce it without a code deploy.
What should a CDN or reverse-proxy config actually enforce?
A hardened configuration should do five concrete things. First, strip or overwrite unkeyed headers the origin trusts — X-Forwarded-Host, X-Forwarded-Scheme, X-Original-URL, X-HTTP-Method-Override — at the edge, setting them to known-good values rather than passing through client-supplied ones. Second, audit every Vary header for accuracy: if a response genuinely differs based on a header, that header must be either in the cache key or in Vary, with no exceptions. Third, normalize and canonicalize query strings, and disable caching on parameter combinations that aren't explicitly allow-listed, since arbitrary query-string caching multiplies the attack surface. Fourth, configure cache keys (via Cloudflare Cache Rules, Fastly VCL, or CloudFront cache policies) to key on every input the backend actually consumes for a given route, not the platform default. Fifth, exclude responses carrying Set-Cookie or any authenticated content from caching entirely, and apply strict path-suffix rules so extension-based static-asset caching can't be tricked into storing dynamic, per-user responses — directly closing the web cache deception vector.
Why does this matter for teams focused on supply-chain security?
Cache poisoning is a delivery-layer risk rather than a dependency risk, but it belongs in the same threat model as software supply-chain security because both exploit trust boundaries an application doesn't fully control — one at the CDN/reverse-proxy layer, the other in third-party packages and build pipelines. A team that has hardened its dependency graph, generates SBOMs on every release, and tracks CVEs across its supply chain can still ship a poisonable cache configuration if header trust and cache-key scope were never audited as part of the same security review. Treating edge configuration as a reviewable, versioned artifact — auditing Vary accuracy and unkeyed-header handling with the same rigor applied to dependency manifests — closes a gap that a purely code-focused or purely dependency-focused security program will otherwise miss entirely.