Security

Web Cache Poisoning: How It Works and How to Prevent It

Understand web cache poisoning via unkeyed headers, cache-busting parameters, and fat GET requests — and how to defend with Vary headers and CDN configuration.

March 9, 20265 min readShipSafer Team

Web Cache Poisoning: How It Works and How to Prevent It

Web cache poisoning is an attack where an adversary manipulates a caching layer into storing a malicious response and then serving it to legitimate users. Unlike XSS or SQLi, which exploit application code directly, cache poisoning exploits the gap between what the cache uses to identify a request (the cache key) and what the application actually uses to generate the response. If those two things differ, an attacker can inject arbitrary content into the cache.

How Caching Works: Keys and Unkeyed Inputs

Every caching proxy (CDN, Varnish, Nginx, Squid) stores responses indexed by a cache key. By default, this key is derived from the URL and possibly a few headers. When two requests produce the same cache key, the second request gets the cached response from the first.

The vulnerability arises from unkeyed inputs: request headers, parameters, or cookies that influence the response but are not included in the cache key. If the application uses an unkeyed header to construct the response, an attacker who can control that header can influence what the cache stores.

Classic Example: X-Forwarded-Host

Many applications use the Host or X-Forwarded-Host header to construct absolute URLs in responses (for redirects, canonical links, API endpoint references, or resource imports).

A typical scenario:

GET / HTTP/1.1
Host: example.com
X-Forwarded-Host: attacker.com

If the application generates:

<script src="https://attacker.com/static/app.js"></script>

...and this response gets cached, every subsequent visitor to example.com/ who gets the cached response loads JavaScript from attacker.com. The attacker now controls script execution for every visitor — without ever needing to compromise the origin server.

Unkeyed Query Parameters

Some caching configurations key on the URL path but strip or ignore certain query parameters. Marketing tracking parameters (utm_source, fbclid) are commonly excluded from cache keys so that /?utm_source=email and /?utm_source=twitter get the same cached response.

If the application reflects these parameters in the response (say, in an analytics script tag or an open-graph meta tag), an attacker can use them as injection vectors:

GET /?utm_source=%22%3E%3Cscript%3Ealert(1)%3C/script%3E HTTP/1.1

If this value appears unescaped in the cached response, every visitor gets the XSS payload served from the cache.

Fat GET Requests

Some frameworks treat the request body of a GET request as additional parameters. If the caching layer does not include the request body in the cache key (which is typical — bodies are rarely keyed), an attacker can use the body to influence the response while the cache keys on only the URL:

GET /search?q=example HTTP/1.1
Content-Length: 25
Content-Type: application/x-www-form-urlencoded

q=<script>alert(1)</script>

If the application uses the body parameter in preference to the query string parameter, the poisoned response gets cached under the clean URL key.

Detecting Cache Poisoning Vulnerabilities

Researcher James Kettle (Portswigger) documented the methodology for finding these vulnerabilities systematically. The core technique is:

  1. Identify cache keys — add a random cache-buster parameter (?cb=RANDOM) to get fresh responses
  2. Add headers one by one and check if they are reflected in the response
  3. If a header is reflected but not in the cache key, test whether it is cacheable
# Test if X-Forwarded-Host is reflected
curl -s -H "X-Forwarded-Host: canary12345.example.com" \
  "https://target.com/?cb=$(date +%s)" | grep "canary12345"

# If reflected, test without cache-buster to see if it caches
curl -s -H "X-Forwarded-Host: canary12345.example.com" \
  "https://target.com/"

# Check if a clean request now gets the poisoned response
curl -s "https://target.com/" | grep "canary12345"

Prevention: Vary Header

The HTTP Vary header tells caches which request headers to include in the cache key. If your application uses X-Forwarded-Host to build responses, include it in Vary:

Vary: Accept-Encoding, X-Forwarded-Host

With this header, requests with different X-Forwarded-Host values are cached separately. An attacker can poison the cache entry for requests with their injected header, but legitimate users (who do not send X-Forwarded-Host) get a different cache entry.

Prevention: Strip Unkeyed Headers at the CDN

The better approach is to strip headers the application should not be using for response construction before they reach the origin:

Cloudflare Transform Rules — strip X-Forwarded-Host before it reaches origin:

Field: X-Forwarded-Host
Operation: Remove

AWS CloudFront — use the X-Forwarded-For policy and explicitly block forwarding of dangerous headers in your origin request policy.

Nginx reverse proxy — explicitly unset dangerous headers:

proxy_set_header X-Forwarded-Host "";
proxy_set_header X-Original-URL "";
proxy_set_header X-Rewrite-URL "";

Prevention: Use Relative URLs

Many of these attacks only work because the application uses headers to construct absolute URLs. Switching to relative URLs for resources, redirects, and canonical links removes the injection point entirely:

<!-- Vulnerable — uses header to build absolute URL -->
<script src="https://example.com/static/app.js"></script>

<!-- Safe — relative URL -->
<script src="/static/app.js"></script>

Prevention: Cache-Control for Dynamic Content

Ensure dynamic content that processes request headers is not cached:

Cache-Control: no-store, private

For API responses that are personalized per user or that reflect any request input, no-store is the safest option. It instructs all caches (CDN and browser) not to store the response.

CDN-Specific Hardening

Each CDN has slightly different defaults and gotchas:

  • Cloudflare: Use Cache Rules to define exactly which URL patterns are cached. Strip query parameters you do not want keyed using Transform Rules.
  • CloudFront: Use cache policies to explicitly define what goes into the cache key. Do not use the legacy "forward all headers" setting.
  • Fastly: Use VCL to explicitly construct cache keys and strip unneeded inputs.

Audit your CDN configuration regularly with a tool like Portswigger's Web Cache Vulnerability Scanner or by manually testing with cache-buster parameters. Cache poisoning vulnerabilities are common in complex CDN configurations and are often not caught by standard vulnerability scanners.

Check Your Security Score — Free

See exactly how your domain scores on DMARC, TLS, HTTP headers, and 25+ other automated security checks in under 60 seconds.