Web cache poisoning is a class of vulnerability that turns the caching layer — a component designed to improve performance — into a delivery mechanism for malicious responses. A successful cache poisoning attack can affect thousands of users from a single injected request, making it one of the highest-impact vulnerabilities in a category that is still systematically underfound in security assessments.
The root cause is a fundamental design problem: caches must decide which responses are equivalent and can be shared across users. They make this decision based on a subset of request properties — the cache key. Any properties that affect the response but are not included in the cache key become potential attack vectors. These are unkeyed inputs.
The Mechanics of Cache Poisoning
A web cache stores responses indexed by cache key, typically comprising the URL and sometimes the Host header. When a response is cached, subsequent requests with a matching key are served the stored response — regardless of what other headers those subsequent requests contain.
The poisoning attack exploits the gap between what the cache keys on and what the application uses to generate the response:
- Attacker sends a request including a malicious unkeyed header (e.g.,
X-Forwarded-Host: evil.com) - The application reflects this header in the response (e.g., in a script tag URL,
<link>, orLocationheader) - The cache stores this response, keyed on the URL without the malicious header
- All subsequent users who request that URL receive the poisoned response
The cached response now contains content controlled by the attacker — affecting every user until the cache expires.
Common Unkeyed Input Sources
Forwarding and Protocol Headers
These headers are commonly used by applications to determine the original request context but are rarely included in cache keys:
X-Forwarded-Host
X-Forwarded-Scheme
X-Forwarded-Proto
X-Original-URL
X-Rewrite-URL
X-Forwarded-Server
Forwarded
If the application uses X-Forwarded-Host to construct absolute URLs (for redirects, canonical links, or content references) and the CDN doesn’t key on it, poisoning is likely possible.
Vulnerable Python/Flask pattern:
@app.route('/')
def index():
host = request.headers.get('X-Forwarded-Host', request.host)
# host is reflected into response — potential poisoning vector
canonical_url = f"https://{host}/page"
return render_template('index.html', canonical=canonical_url)
Vulnerable Node.js/Express pattern:
app.get('/assets/config.js', (req, res) => {
const apiBase = req.headers['x-forwarded-host'] || req.hostname;
res.setHeader('Content-Type', 'application/javascript');
// If this response is cached, apiBase is attacker-controlled for all users
res.send(`window.API_BASE = "https://${apiBase}/api";`);
});
Unkeyed Query Parameters
Some caching configurations exclude specific query parameters from the cache key — typically to normalise UTM tracking parameters. If the application reflects excluded parameters in the response, they become poisoning vectors.
# Request with excluded utm_content parameter:
GET /page?utm_content="><script>alert(1)</script>
# If utm_content is excluded from cache key but reflected in response,
# the poisoned response is cached and served to all users hitting /page
HTTP Method Override Headers
X-HTTP-Method-Override
X-Method-Override
_method
Applications that support method overriding via headers may behave differently based on these headers, creating a response divergence that a cache stores incorrectly.
Fat GET Requests
Some frameworks process request bodies in GET requests. If a cache serves the response to all GET requests regardless of body content, an attacker can include a body that alters the response — and the cache stores and serves it broadly.
Web Cache Deception (The Inverse Attack)
Web cache poisoning and web cache deception are related but distinct:
- Poisoning: Attacker crafts a response the cache stores and serves to others
- Deception: Attacker tricks the cache into storing a victim’s private response, then retrieves it
Cache deception exploits path confusion between the origin server and cache. If the cache serves static files for any URL ending in .css or .js but the origin server renders dynamic content at that path, an attacker can:
- Trick a victim into visiting
/account/profile.css(via phishing, CSRF, etc.) - The origin serves the dynamic account page at that path
- The cache stores it as a static asset (because of the
.cssextension) - The attacker retrieves
/account/profile.cssand receives the victim’s account page
Finding Cache Poisoning Vulnerabilities
Step 1 — Identify unkeyed headers. Send requests with added headers containing a distinctive value (X-Forwarded-Host: cache-test-12345.example.com) and observe whether the value appears in the response. If it does, and the response is cacheable, you likely have a poisoning vector.
# Test X-Forwarded-Host reflection
curl -s -I -H "X-Forwarded-Host: cache-poison-test.attacker.com" https://target.com/ | grep -i location
curl -s -H "X-Forwarded-Host: cache-poison-test.attacker.com" https://target.com/ | grep -i "cache-poison-test"
Step 2 — Verify cacheability. Check response headers for caching indicators:
Cache-Control: public, max-age=3600 ← cacheable
Age: 234 ← served from cache
X-Cache: HIT ← CDN cache hit
Cf-Cache-Status: HIT ← Cloudflare cache hit
Step 3 — Confirm poisoning. After sending the malicious request, send a clean request (no custom headers) and verify whether the poisoned response is returned.
Step 4 — Assess impact. What is reflected in the response? Possibilities include:
- URL in
<script src="">tag — JavaScript injection, full XSS Locationredirect header — open redirect, phishing<link rel="canonical">— SEO impact, may not be exploitable for XSS- JSON response field — depends on consumer context
Fixing Cache Poisoning
At the CDN/Cache Layer
Add unkeyed headers to the cache key. Most CDNs allow customisation of the cache key. If your application uses X-Forwarded-Host, include it in the key so different host values produce separate cache entries.
# Nginx — add X-Forwarded-Host to cache key
proxy_cache_key "$scheme$request_method$host$request_uri$http_x_forwarded_host";
Strip headers the application shouldn’t trust. If the origin doesn’t need X-Forwarded-Host, strip it at the CDN layer before forwarding to origin.
# Cloudflare Workers — strip forwarding headers
const sanitised = new Request(request);
sanitised.headers.delete('X-Forwarded-Host');
sanitised.headers.delete('X-Original-URL');
Set a strict Vary header. The Vary header tells caches which request headers produce different responses. If X-Forwarded-Proto affects the response, declare it:
Vary: Accept-Encoding, X-Forwarded-Proto
At the Application Layer
Don’t trust forwarding headers for security-sensitive operations. Prefer explicit configuration over request headers for determining the application’s base URL:
# BAD — trusts X-Forwarded-Host
host = request.headers.get('X-Forwarded-Host', request.host)
# GOOD — configured base URL, not derived from request
BASE_URL = os.environ.get('APP_BASE_URL', 'https://myapp.example.com')
Validate forwarding headers against an allowlist. If you must use forwarding headers, validate them against known-good values:
const ALLOWED_HOSTS = new Set(['myapp.com', 'www.myapp.com', 'api.myapp.com']);
function getTrustedHost(req) {
const forwarded = req.headers['x-forwarded-host'];
if (forwarded && ALLOWED_HOSTS.has(forwarded)) {
return forwarded;
}
return req.hostname; // fall back to server-set value
}
Mark truly uncacheable responses as such:
Cache-Control: no-store, private
Testing in CI
Include cache poisoning checks in your security automation. Burp Suite’s Web Cache Poisoning scanner covers the main vectors. For open-source tooling, param-miner (a Burp extension) automates header and parameter mining. The cachepoisoner tool and manual curl scripts using the patterns above can be integrated into pipeline security gates.
Cache poisoning is underfound not because it is rare, but because standard DAST scanning often misses it. A dedicated check as part of your security review process consistently finds high-impact vulnerabilities before production.