Website Security

Web Cache Deception: When Your CDN Caches a Stranger's Account Page

2MNY Admin October 13, 2026 4 min read 4 views

A victim clicks a link to /account/settings/style.css. The router normalizes the unknown suffix and serves the real account page — but the cache layer, keying on .css, stores the personalized response for everyone. Minutes later the attacker fetches the same URL and receives the victim's cached page. Here's how the attack works, how we prove it safely, and the three-layer defense.

Web cache deception is a 2017-class attack that keeps working in 2026 because it lives in the gap between two components that were never designed to agree: your router, which normalizes paths to find something to serve, and your cache layer, which decides what to store based on file extensions. When the router says "I don't know what style.css is, I'll serve the account page" and the cache says ".css? that's a static asset, cache it for everyone" — the victim's private page becomes a public object with a URL you can share.

The Attack, Step by Step

  1. The attacker needs the victim to request a crafted URL on the target site: /account/settings/style.css. Delivery is routine: an img tag in a comment, a link in a chat message, a redirect from an attacker page.
  2. The victim's browser requests it. The site's routing treats the unrecognized suffix as noise and serves the dynamic page for /account/settings — rendered with the victim's session: their email, their API tokens, their PII.
  3. The cache layer in front (CDN, reverse proxy, full-page cache) makes its storage decision from the URL: .css means static, cacheable, share it. It stores the personalized response.
  4. The attacker requests the same URL. The cache serves it — the victim's page, from the cache, no session required.

No exploit payload. No memory corruption. Just two well-meaning components disagreeing about what a URL means, at the victim's expense.

Why Detection Is Subtle

A naive check fetches /some/page/style.css and reports "cache deception" if it returns 200. That produces false positives in two directions. First, catch-all routers: many apps serve the homepage (or a themed 404) for any unknown path — that 200 isn't your account page, it's the router shrugging, and it's harmless. Second, actual asset handling: some frameworks have real extension-aware routes that legitimately serve CSS.

The proof has to show that the response to the asset-path variant is the full dynamic page, and that the cache actually stored it. That's how our http_cache_deception scanner works, in six steps:

  1. Soft-404 calibration — two nonce paths establish what this server's "unknown path" behavior looks like, so a catch-all router can never fake a hit (the central Soft404Guard every 2MNY scanner shares).
  2. Candidate selection — up to three dynamic pages from the scan surface: the homepage, an auth-flavored page (discovered or /login), and one crawled page.
  3. Variant probes — five asset-path shapes per candidate: /style.css, /x.css, URL-encoded %2fstyle.css, the semicolon trick ;.css, and /test.js. Different caches and routers fall for different variants.
  4. Full-page confirmation — a variant counts only if the response is 200, the content-type is HTML (not css/js), it isn't a soft-404, and its token-similarity to the page baseline exceeds 60% (simhash plus title match). In other words: this is the actual page, served under an asset path.
  5. Cacheability signals — cache-control public or max-age > 0, an age header, x-cache hit headers, CDN status headers, proxy-cache headers. Full page plus cache signals = high-severity web cache deception. Full page without cache signals = medium-severity path-confusion precondition — the router misbehaves today, and the day someone adds a cache layer, it becomes the attack.
  6. Re-verification — on a positive, the winning variant is re-fetched to confirm the behavior is stable, not a one-off rendering fluke.

The entire probe set is GET requests against public paths — no payloads are stored on the target, no victim session is ever involved. Detection proves the class safely: if your router serves account pages under /style.css, that's the vulnerability, whether or not a cache was watching during the scan.

The Three-Layer Defense

Fix the router (the root cause). Unknown paths must 404, not normalize. If your framework's fallback route serves app pages for arbitrary extensions, constrain the fallback: extension-bearing paths should never resolve to dynamic templates. This alone kills the whole class — including variants nobody has invented yet.

Fix the cache policy (the blast radius). Cache allow-list, never deny-list: static assets get cached, everything else gets Cache-Control: private, no-store by default. The failure mode of "cache everything that looks static" is this entire article.

Fix the page (the payload value). Anything session-bearing should set explicit Cache-Control: private headers regardless of what the infrastructure defaults are — defense in depth for the day the router and the cache disagree about something new.

Then verify from the outside: the free 2MNY scan runs the five-variant probe set with baseline comparison and cache-signal grading on every scan, so both the path-confusion precondition and a fully cached leak surface as distinct, evidence-backed findings — before someone shares a /style.css link your CDN is proud of.

Your cache exists to serve strangers. Your account pages shouldn't be in it.

Is your website affected?

Find out in minutes — free scan, professional report, no credit card.

Start Free Scan