Web Cache Poisoning: How Attackers Weaponize CDNs and Reverse Proxies
What Is Web Cache Poisoning?
Web cache poisoning tricks a caching layer (CDN, reverse proxy, load balancer) into storing a malicious response and serving it to all subsequent visitors.
Normal flow:
User → CDN Cache (cached /page) → 200 OK (clean page)
Poisoned flow:
Attacker → Origin Server → 200 OK (malicious response injected via headers)
↓
CDN Cache stores poisoned response
↓
ALL subsequent visitors → CDN Cache → 200 OK (malicious page!)
Impact: One request from an attacker → XSS/defacement/phishing served to every visitor until cache expires.
How Cache Poisoning Works
The attack relies on unkeyed inputs — parts of the request that the cache ignores when generating the cache key, but the origin server uses in the response.
Cache Key vs. Full Request
Full Request:
GET /page HTTP/1.1
Host: target.com
X-Forwarded-Host: evil.com ← Unkeyed (not in cache key)
Accept-Language: en ← Unkeyed
Cookie: session=abc123 ← Usually unkeyed
Cache Key (what CDN uses to match requests):
GET target.com/page
The CDN sees the same "page" for all visitors, regardless of
X-Forwarded-Host or Accept-Language headers.
Attack 1: X-Forwarded-Host Header Poisoning
Many frameworks use X-Forwarded-Host to generate absolute URLs for assets, links, and redirects.
# Attacker's request
GET / HTTP/1.1
Host: target.com
X-Forwarded-Host: evil.com
# Origin response (using X-Forwarded-Host for asset URLs)
<html>
<script src="https://evil.com/assets/main.js"></script>
<link rel="stylesheet" href="https://evil.com/assets/style.css">
</html>
# CDN caches this response for the cache key "GET target.com/"
# All visitors now load JavaScript from evil.com!
Defense
// ✅ Never use X-Forwarded-Host without validation
const ALLOWED_HOSTS = ['target.com', 'www.target.com'];
function getHost(req: Request): string {
const forwarded = req.headers['x-forwarded-host'];
if (forwarded && ALLOWED_HOSTS.includes(String(forwarded))) {
return String(forwarded);
}
return req.headers.host || ALLOWED_HOSTS[0];
}
// ✅ Better: Use a fixed base URL from config
const BASE_URL = process.env.BASE_URL; // https://target.com
Attack 2: Unkeyed Header Reflection
# Find headers reflected in response
GET / HTTP/1.1
Host: target.com
X-Custom-Header: <script>alert(1)</script>
# If the response includes:
# <meta name="custom" content="<script>alert(1)</script>">
# And the CDN caches it — stored XSS for all visitors
Testing Methodology
# Step 1: Find unkeyed headers using Param Miner (Burp extension)
# Or manually test common headers:
X-Forwarded-Host
X-Forwarded-Scheme
X-Original-URL
X-Rewrite-URL
X-Forwarded-Proto
X-Host
# Step 2: Check if header value appears in response
# Step 3: Verify the response gets cached (check Age, X-Cache headers)
# Step 4: Confirm other users get the cached poisoned response
Attack 3: Fat GET / POST Body Poisoning
Some caching systems treat GET requests with a body the same as regular GETs.
# Cache key: GET /api/user (no body considered)
GET /api/user HTTP/1.1
Host: target.com
Content-Length: 30
{"role": "admin", "id": "1"}
# If the origin uses the body to customize the response,
# the cache stores the admin response for /api/user
# All users now see admin data!
Attack 4: Cache Key Normalization Exploits
# Some CDNs normalize URLs before generating cache keys:
# /page and /PAGE might share the same cache key
# /page and /page? might share the same cache key
# But the origin server might treat them differently,
# allowing you to poison the normalized version
Detection: Check Your Cache Headers
# Look for these response headers to understand caching behavior
curl -sI https://target.com | grep -i 'cache\|age\|x-cache\|cf-cache\|vary'
# Key headers:
# Cache-Control: max-age=3600 → Response is cached for 1 hour
# Age: 542 → Response has been cached for 542 seconds
# X-Cache: HIT → Response served from cache
# Vary: Accept-Encoding → Cache key includes Accept-Encoding
# CF-Cache-Status: DYNAMIC → Cloudflare not caching (safe)
CDN-Specific Defenses
Cloudflare
# Cache Rules → Only cache static assets
# Page Rules → Cache Level: Bypass for dynamic pages
# Transform Rules → Strip unrecognized headers before origin
CloudFront
{
"CacheBehavior": {
"ForwardedValues": {
"Headers": ["Host"],
"QueryString": true
},
"CachePolicy": {
"HeadersConfig": {
"HeaderBehavior": "whitelist",
"Headers": ["Host", "Accept-Encoding"]
}
}
}
}
Cache Poisoning Prevention Checklist
- Use fixed base URLs — never generate URLs from request headers
- Strip unknown/custom headers at the CDN/proxy level
- Ensure
Varyheader includes all inputs that affect the response - Don't cache responses that vary based on cookies or custom headers
- Cache only static assets — bypass cache for dynamic/personalized content
- Set
Cache-Control: private, no-storefor authenticated responses - Monitor cache HIT rates for anomalies
- Test with tools like Param Miner to find unkeyed inputs
- Use
Surrogate-ControlorCDN-Cache-Controlfor CDN-specific cache rules
Advertisement
Free Security Tools
Try our tools now
Expert Services
Get professional help
OWASP Top 10
Learn the top risks
Related Articles
Threat Modeling for Developers: STRIDE, PASTA & DREAD with Practical Examples
Threat modeling is the most cost-effective security activity — finding design flaws before writing code. This guide covers STRIDE, PASTA, and DREAD methodologies with real-world examples for web, API, and cloud applications.
Building a Security Champions Program: Scaling Security Across Dev Teams
Security teams can't review every line of code. Security Champions embed security expertise in every development team. This guide covers program design, champion selection, training, metrics, and sustaining engagement.
The Ultimate Secure Code Review Checklist for 2025
A comprehensive, language-agnostic checklist for secure code reviews. Use this as your team's standard for catching vulnerabilities before they reach production.