Web Cache Poisoning: Your CDN Is Serving Malware to Everyone 🐍
Web Cache Poisoning: Your CDN Is Serving Malware to Everyone 🐍
Picture this: your CDN is humming along, caching responses, making your site scream-fast. Users are happy. Your boss is happy. You're happy.
Then some attacker crafts one sneaky request. Your cache stores the poisoned response. And now your CDN — the infrastructure you pay good money for — is faithfully distributing malicious content to every single visitor.
That's web cache poisoning. And in my years building production e-commerce backends behind CloudFront and Nginx, I've seen exactly how easy it is to set this trap accidentally.
What Is Web Cache Poisoning? 🎯
Caches work on a simple principle: same request = serve the stored response. No need to hit the origin server again.
The key word is same request. Caches typically key responses on URL + method + maybe a few headers. But your backend might look at other headers — headers the cache completely ignores — to build the response.
That gap? That's the attack surface.
Attacker Request:
GET /home HTTP/1.1
Host: example.com
X-Forwarded-Host: evil.com ← cache ignores this
Backend Response (now cached):
<script src="https://evil.com/malicious.js"></script>
Every subsequent visitor gets... the poisoned response. 😱
The attacker's request is processed once. The cache stores it. Everyone else gets poisoned without sending anything unusual themselves.
The Classic: Unkeyed Header Injection 💉
In my experience building production systems, the most common vector I see is unkeyed headers that influence the response body.
Dangerous pattern:
// Backend grabs this to build absolute URLs
$host = $_SERVER['HTTP_X_FORWARDED_HOST'] ?? $_SERVER['HTTP_HOST'];
// Now used in response
echo "<script src='https://{$host}/assets/app.js'></script>";
What happens:
- Attacker sends
X-Forwarded-Host: attacker.com - Backend generates
<script src="https://attacker.com/assets/app.js"></script> - Cache stores this response (it only keyed on URL + Host — not
X-Forwarded-Host) - Next 10,000 visitors load
attacker.com/assets/app.js - Attacker serves whatever JavaScript they want to your entire user base
The fix:
// ONLY trust headers your infrastructure explicitly sets
// Never trust X-Forwarded-Host from arbitrary clients
$host = config('app.url'); // Use your configured base URL
// If you MUST use forwarded headers, whitelist them
$allowed_hosts = ['example.com', 'www.example.com'];
$forwarded_host = $_SERVER['HTTP_X_FORWARDED_HOST'] ?? '';
$host = in_array($forwarded_host, $allowed_hosts) ? $forwarded_host : 'example.com';
The "Fat GET" Variant 🐷
Some frameworks and proxies handle GET requests with a body. The cache keys on the URL. The backend reads the body. Classic mismatch.
GET /search?q=laptops HTTP/1.1
Content-Length: 20
Content-Type: application/x-www-form-urlencoded
q=<script>alert(1)</script>
Cache sees: GET /search?q=laptops → caches the XSS'd response. Everyone searching for laptops gets a surprise.
Real Talk 💬
As someone passionate about security, I've spent time on bug bounty programs, and "fat GET" issues are still surprisingly common in 2026. Frameworks like Express handle them differently. Proxy servers cache them differently. That inconsistency is gold for attackers.
Cache Deception vs Cache Poisoning — They're Different! 🤔
People mix these up constantly. In security communities, we often discuss both, but they're distinct:
| Cache Poisoning | Cache Deception | |
|---|---|---|
| Goal | Poison cache, attack others | Trick cache into storing your private data |
| Victim | All subsequent users | The authenticated user |
| Attack | Inject malicious content into cache | Get cache to store user's private response |
| Example | Serve XSS to everyone | Cache someone's account page, read it later |
Both are brutal in production. I'm focused on poisoning today, but bookmark deception too.
Parameter Cloaking 🕵️
Modern CDNs often have quirks in how they parse query strings. Your origin and your cache might disagree on what ? means.
# Cache sees this key:
GET /api/user?id=1
# But attacker sends:
GET /api/user?id=1;id=admin
# Cache: "just /api/user?id=1" — matches!
# Backend: reads the LAST id parameter → id=admin
This depends heavily on your CDN and backend. But in security communities, we've seen it exploited against major platforms.
Pro Tip 🔑
Run your app through Param Miner (Burp Suite extension). It automatically discovers unkeyed inputs by fuzzing headers and parameters. James Kettle's research on this is the definitive resource — he found this class of vulnerabilities at scale.
How to Actually Protect Your Production App 🛡️
After incidents and research, here's what I enforce on every system I build:
1. Normalize Your Cache Keys
# Nginx: explicitly strip headers that shouldn't vary caching
proxy_cache_key "$scheme$request_method$host$request_uri";
# CloudFront: in your cache policy, ONLY include headers you explicitly need
# Default: key on Host + URL only
# Add headers only if your backend NEEDS them for the response
2. Sanitize Forwarded Headers at the Edge
// In Laravel — don't trust TrustProxies blindly
// app/Http/Middleware/TrustProxies.php
protected $proxies = [
'10.0.0.0/8', // Only your load balancers
'172.16.0.0/12',
];
// NOT: $proxies = '*'; // This trusts every IP — dangerous!
3. Cache-Control on Sensitive Responses
// Responses that contain user-specific data
// should NEVER be cached by shared caches
return response()->json($userData)
->header('Cache-Control', 'private, no-store');
// Public, safe-to-cache responses
return response()->json($publicData)
->header('Cache-Control', 'public, max-age=3600');
4. Vary Header — Use It Correctly
// If your response changes based on Accept-Language,
// tell the cache to key on it
return response($content)
->header('Vary', 'Accept-Language');
// WARNING: Vary: * means "never cache this" — useful for
// responses that genuinely differ per user
return response($userContent)
->header('Vary', '*');
The Audit Checklist 📋
In my experience, these are the first things I check when auditing a system for cache poisoning:
- What headers does your backend read to build responses? Are any unkeyed?
- Does
X-Forwarded-Host,X-Original-URL, orX-Rewrite-URLaffect the response? - Does any header end up reflected in the response (in HTML, JSON, redirects)?
- Are your CDN cache policies explicitly defined, or left on defaults?
- Do your GET endpoints process request bodies?
- Are authenticated/user-specific responses marked
Cache-Control: private?
If you answer "I don't know" to any of these, that's where to start.
The Real World Story 🌍
During a security review for an e-commerce client, I found their product recommendation API reading X-Country-Code to return region-specific products. The CDN wasn't keying on this header.
An attacker could send X-Country-Code: XX where XX triggered a weird edge case that reflected the header value directly in the JSON response. Not a full XSS since it was JSON, but the poisoned response meant every user in that cache zone got wrong product data — and in a hypothetical worse implementation, it could've been script injection.
Two-line fix: strip X-Country-Code at the CDN edge and let the origin derive it from IP geolocation instead. Cache now correctly serves identical responses.
TL;DR — Cache Poisoning in 30 Seconds ⚡
- Attacker finds a header/param the cache ignores but your backend uses
- Attacker crafts a request that makes your backend generate a malicious response
- Cache stores the poisoned response
- Everyone else receives the attacker's payload — automatically, at CDN speed
Protect yourself:
- Explicitly define what your cache keys on (don't use defaults blindly)
- Never trust arbitrary client headers for response generation
- Mark user-specific responses
Cache-Control: private - Audit headers your backend reads — if it's not in the cache key, it's a risk
This is one of those vulnerabilities where the damage scales with your traffic. The bigger your CDN footprint, the more users get hit. So the time to audit is before you're serving a million users, not after.
Enjoyed this? Drop me a message on LinkedIn — I love talking about this stuff with other practitioners.
GitHub: github.com/kpanuragh — occasionally I publish security tooling and research notes there too.
Stay paranoid. Cache responsibly. 🔐