Cache Key Confusion: Finding Web Cache Bugs That Matter
A cache key is the CDN's answer to one question: which requests are equivalent? Trouble starts when the application cares about an input that the cache ignores. Two requests look identical to the cache, different to the origin, and one user's response gets handed to another.
This is broader than classic cache poisoning. Sometimes the prize is a stored redirect or script include. Sometimes it is a private dashboard cached under a public key. The work is figuring out exactly what joins the key.
Build a key map
Pick a low-risk page and add a cache buster such as ?cb=48192. Repeat it until headers like Age, X-Cache, or CF-Cache-Status show a hit. Then vary one input at a time:
- Query parameter order, duplicate parameters, and ignored tracking keys
Host,X-Forwarded-Host,X-Original-URL, and rewrite headers- Cookies, authorization state, language, device, and compression headers
- Path case, dot segments, encoded delimiters, and a trailing slash
If changing an input changes the origin response but the second request still hits the first cached object, you found a key mismatch. Use a unique text canary so the result is obvious.
Here's a common shape. X-Forwarded-Host: canary.example changes an absolute password-reset link rendered by the app, but the CDN keys only on the URL. Your priming response stores that hostile host; the next visitor receives it without supplying the header.
Confusion across authentication
The scarier test compares two accounts you own. Visit /settings as account A, confirm whether it is cacheable, then request the same URL as B or logged out. A response containing A's canary name proves cross-user leakage. Do not test this by waiting for a stranger's response.
Pay close attention to cookies the origin reads but the edge does not vary on. A theme cookie is boring. A tenant, currency, or experiment cookie can alter sensitive content or HTML execution context. Also test whether a path that looks static to the CDN—/avatar/user.jpg—is dynamically routed by the backend.
Cache deception versus poisoning
Cache poisoning stores attacker-influenced content for later visitors. Cache deception tricks the cache into storing a victim-specific response under a public-looking path. A route such as /account/profile.css may be treated as private by the app but static by the CDN. Ask your own victim account to visit it, then fetch it unauthenticated and look for the private response.
Do not claim site-wide impact from one local cache node. Record Age, cache status, geography if relevant, and expiry. Prove persistence with two clean clients. A one-off reflected header is not poisoning unless another request receives it.
A report triage can reproduce
Give a three-request sequence: clean miss, poisoned or victim-authenticated fill, then independent hit without the special input. Include only canary data. State the cache-key omission in one sentence and identify the affected content.
Fixes should match the bug: include every response-changing input in the key, strip untrusted forwarding headers, mark personalized responses private, no-store, and align CDN path normalization with the application router. Purging the object cleans today; correcting equivalence prevents tomorrow.