Back to Glossary
high severity · Vulnerability Glossary

Web Cache Poisoning

Updated August 24, 2026

4 min read

web cache poisoning
cache poisoning
cache key
unkeyed input
X-Forwarded-Host
CDN cache poisoning
web cache deception
Varnish cache poisoning

Web cache poisoning is an attack where one crafted request makes a shared cache — a CDN edge, a reverse proxy, a Varnish node — store a malicious response and serve it to every later visitor of that URL. One request, every subsequent victim. The fix is configuration, not a rewrite. Rated high for blast radius, with the honest precondition that it only works when an unkeyed input measurably changes a response the cache will store.

What is web cache poisoning?

Every cache decides whether two requests are "the same" using a cache key — typically method, Host, path and query string, and often little else. Everything outside the key is unkeyed input: headers such as X-Forwarded-Host, X-Forwarded-Scheme, X-Original-URL and X-Rewrite-URL, which frameworks and load balancers read to build absolute URLs and routes.

The vulnerability is the mismatch: an input that changes the response but not the key. Poison the response once and the cache files it under the innocent key real users ask for. Cache-key normalisation flaws and web cache deception share that root cause. In the OWASP Top 10:2025 it falls under A02: Security Misconfiguration (A05 in 2021).

Why it's a risk / how it's exploited

Say the app builds its asset tags from the forwarded host, emitting <script src="https://{X-Forwarded-Host}/static/app.js">. An attacker sends GET / with X-Forwarded-Host: evil.example; if the edge stores that response under the plain / key, every visitor loads attacker-controlled JavaScript for the whole TTL — stored XSS across your user base.

PortSwigger's multi-header technique pairs X-Forwarded-Scheme: http (any value other than https) with X-Forwarded-Host: evil.example so the origin answers with a 301 to the attacker's domain; cache that and you have a site-wide open redirect. Where the attacker influences response headers directly, CRLF injection makes poisoning far easier.

The preconditions are real: a shared cache, a cacheable response, and an unkeyed input that measurably alters the output. Miss one and there is no finding. CDNs cache per POP, so a poisoned entry may reach only one region.

How to detect it

Use a unique cache-buster so the only key you can poison is your own, and never test a cache you do not own — a successful test hits real users.

CB=$(date +%s)
# 1. Does the header change the response?
curl -s "https://example.com/?cb=$CB" \
  -H 'X-Forwarded-Host: canary.invalid' | grep -o 'canary\.invalid'

# 2. Same URL, no header — did the cache keep it?
curl -si "https://example.com/?cb=$CB"

Vulnerable: step 1 prints canary.invalid and step 2 returns X-Cache: HIT (or CF-Cache-Status: HIT, or an Age above 0) with the canary still in the body. Safe: no reflection, or Cache-Control: no-store and a permanent MISS. Use -si, not -I — HEAD returns no body.

How to fix it

Strip the forwarding headers at the trusted edge unless the app needs them. In nginx (http, server or location context) an empty value stops the field reaching the upstream — but any proxy_set_header discards inherited ones, so re-declare Host:

proxy_set_header X-Forwarded-Host   "";
proxy_set_header X-Forwarded-Scheme "";
proxy_set_header X-Original-URL     "";
proxy_set_header X-Rewrite-URL      "";

Apache, with mod_headers loaded:

RequestHeader unset X-Forwarded-Host
RequestHeader unset X-Forwarded-Scheme
RequestHeader unset X-Original-URL
RequestHeader unset X-Rewrite-URL

If a header must influence the response, key on it instead. Varnish 4+ — no return (lookup), so the builtin hash still adds host and URL:

sub vcl_hash {
    if (req.http.X-Forwarded-Host) {
        hash_data(req.http.X-Forwarded-Host);
    }
}

Send Cache-Control: no-store on authenticated routes. Vary is the standards-compliant alternative, but CDNs honour it on custom headers inconsistently and Cloudflare's custom cache keys are Enterprise-only.

Edge stripping does nothing if the origin is reachable directly by IP, so restrict it to the edge's ranges; keying extra headers costs hit rate. After any fix, purge — poisoned entries survive until their TTL expires.

FAQ

Is web cache poisoning the same as web cache deception? No — they run in opposite directions. Poisoning stores an attacker's response under a victim's cache key; deception tricks the cache into storing a victim's authenticated response under a key the attacker can fetch, usually via /account/settings.css. Both come from cache and origin disagreeing about what a URL means.

We're behind Cloudflare — aren't we protected? Only against one family of payloads. Cloudflare's default cache key already includes x-forwarded-host, x-host, x-original-url, x-rewrite-url, forwarded and x-forwarded-scheme (unless its value is http or https), so the textbook X-Forwarded-Host attack is keyed there, not unkeyed. Any other reflected header, or an internal Varnish or nginx proxy_cache tier, is still fair game — the CDN is the cache, so it is the thing being poisoned.


Unkeyed header reflection stays invisible until someone tests for it. Run a free website vulnerability scan to find pages that reflect forwarding headers into cacheable responses, or read more on web application security testing.