Web cache deception (WCD) is when an attacker tricks the cache into storing a private, dynamic response by taking advantage of the differences in how the cache and the origin server each interpret a URL. The victim is lured into requesting an ambiguous URL; the cache mistakes their sensitive response for a cacheable static file and stores it; the attacker then requests the same URL and reads the victim’s cached data. It could either be by clicking a link or through phishing emails.
Web caches
A cache sits between client and origin server. On a cache miss it forwards the request to the origin, stores the response per its rules, then serves it; on a cache hit it serves the stored copy directly.
Cache key — the parts of the request (usually URL path + query string, sometimes headers/content type) used to decide if a stored response matches an incoming request. The cache compares this key against what it has stored to decide whether to serve from memory or forward to the origin.
Cache rules — decide what gets stored. Common types:
- Static file extension rules — match extensions like .css, .js.
- Static directory rules — match a path prefix like /static or /assets.
- File name rules — match exact names like robots.txt, favicon.ico. The cache doesn’t store dynamic pages which are the private pages, so it doesn’t store user private data, which is exactly why these rules exist — it stores by static file extension, static directory, and common file name rules, like the examples above.
Exploiting the Cache
In the PortSwigger lab, I targeted a URL that served a private, user-specific page,“https:// 0a260049032f207080a25840006d006a.web-security-academy.net/my-account”, using Burp Suite, I logged in as a user to access my API key.
Using Burp, I sent the my-account request to the Repeater — this is where the vulnerability lives, I appended “x.css” to the a dynamic, private page’s URL and sent the request, The first response came back as a cache miss.
On the second response, after delivering the payload from the exploit server to the client website I got a cache hit containing the client’s API key, meaning the private page had been cached. Here’s what happened: the first request was for a private, dynamic page that should never be cached. But with x.css appended, the origin server ignored the suffix, still resolved the path to /account, and served the private page anyway. The cache, however, only looked at the URL ending — saw .css — and stored the response as if it were a static file.
I logged in as Wiener, then repeated the attack to obtain Carlos’s API key — this is exactly what a real attacker would do: send the ambiguous URL to Carlos, perhaps via a phishing email. When Carlos clicks the link and logs in, his private API key gets cached. The attacker then simply revisits that same URL and reads Carlos’s cached data.
Preventing web cache deception
- Mark all dynamic responses with Cache-Control: no-store, private. So, you set that header on every authenticated or personalized response, — for an Express/Node is
“res.set(‘cache response’, ‘no store, private’)”
.
- Make sure CDN/cache configuration never overrides that header with its own caching rules. You create rules so the CDN respects the header (Cache-Control: no-store, private) response coming from the Origin server.
- Turn on any CDN-native WCD protections (e.g. Cloudflare’s Cache Deception Armor), which typically verify the response Content-Type actually matches the requested file extension.
- Audit for and eliminate discrepancies between how the origin and the cache parse/normalize URL paths. Making sure that both the origin and cache agree on what is to be stored (static resource) and what is not to be stored (dynamic resource)
Conclusion: Web cache deception isn’t a flaw in the cache or the origin server alone — it’s what happens when the two disagree about what a URL means.






Top comments (0)