---
title: "Web Cache Poisoning: How It Happens and How to Prevent It"
description: Learn how web cache poisoning exploits an unkeyed header and cache key—and how to prevent cache poisoning attacks.
---

[Content Delivery Network Blog](https://blog.blazingcdn.com/en-us)

# [Web Cache Poisoning: How It Happens and How to Prevent It](https://blog.blazingcdn.com/en-us/web-cache-poisoning-how-it-happens-and-how-to-prevent-it)

 Written by [Vadym Borovskyi](https://blog.blazingcdn.com/en-us/author/blazingcdn) | Nov 1, 2026, 9:32:44 PM

Cache poisoning happens when a request input that changes the response is left out of the cache key, so an attacker's variant is stored once and served to everyone who asks for the same URL. For example, a page with a 300-second TTL that receives 500 requests per second at one edge serves the poisoned copy 150,000 times before it expires, and the attacker can re-arm it every five minutes. This playbook takes about 90 minutes per zone. You inventory unkeyed inputs, probe them safely, and apply one of three rules to each: key it, strip it, or reject it.

Web cache poisoning works because RFC 9111 defines a shared cache's primary key as the request method and target URI, while origins routinely build responses from inputs outside that key, such as the X-Forwarded-Host header. As of 2026, one crafted request carrying an unkeyed header that alters the body, a redirect target or the status code is enough to poison a cached response for its full TTL, typically 300 to 3,600 seconds on HTML.

## **How cache poisoning happens: unkeyed inputs meet a shared cache key**

Every poisoning bug has the same shape. The CDN decides "these two requests are the same" using the cache key. The origin decides "this is what the response looks like" using a wider set of inputs. Any input in the second set but not the first is a lever an attacker can pull.

Four classes cover nearly every reported case:

- **Unkeyed header reflection.** The origin framework trusts X-Forwarded-Host, X-Forwarded-Scheme or X-Original-URL to build absolute URLs for asset tags, canonical links or redirects. The attacker points them at a host they control, and every visitor loads resources from that host.
- **Cached errors (CPDoS, cache-poisoned denial of service).** The CDN forwards a request the origin rejects: an oversized header past an 8 KB origin limit, a stray meta character, or an X-HTTP-Method-Override that turns a GET into a DELETE. If the 400 or 405 gets cached, the URL is down for everyone until expiry.
- **Unkeyed query parameters.** The CDN drops tracking parameters from the key for hit ratio, but the origin reflects one of them into the page. Parameter cloaking, where the origin and CDN parse delimiters differently, is a variant of this.
- **Fat GET requests.** The origin reads parameters from a GET body that the cache never sees.

Web cache deception is a different bug: the attacker tricks the cache into storing a victim's private response under a static-looking path. It deserves its own audit, and the rules below do not fix it.

### Worked blast radius: why TTL is the multiplier

Blast radius per edge equals request rate on the URL times the TTL. A product page at 50 requests per second with a 600-second TTL yields 30,000 poisoned responses per edge per window. A homepage at 2,000 requests per second with a 60-second TTL yields 120,000 responses. A short TTL does not save the homepage, because the attacker simply re-sends the request after each expiry. The real controls are the key and the inputs the origin trusts, not the TTL.

## **Prerequisites for the audit**

- Read access to your CDN cache key configuration and your CDN's list of headers forwarded to origin.
- Read access to origin code or framework config, specifically anything resolving host, scheme, port or locale from request headers.
- A staging zone, or permission to test in production using unique cache-buster paths so real users never hit a poisoned key.
- An HTTP client that lets you set arbitrary request headers and print response headers, plus your CDN logs with cache status per request.

## **How to find and fix unkeyed headers: the 7-step procedure**

1. **Inventory what the origin reads.** Search the origin codebase and framework settings for every header, cookie and query parameter that influences output. Pay particular attention to trusted-proxy settings, which often accept X-Forwarded-Host from any client. Why: you cannot key or strip what you have not listed.
2. **Write down the effective cache key per route.** Include host, path, which query parameters are kept or dropped, and any headers or cookies added to the key. Why: the gap between this list and step 1 is your attack surface.
3. **Probe each gap with a canary.** For each unkeyed input, send a request to a unique path (append a random cache-buster parameter so you never touch a real user's key) with the input set to a distinctive canary string. Then send a second, clean request to the same unique path. If the clean response contains the canary and shows a cache hit (an Age value above zero or a HIT in your cache-status header), the input is poisonable. Why: the second request proves storage, not just reflection.
4. **Apply the decision matrix below to every vulnerable input.** Why: each input needs exactly one rule, and mixing rules is how gaps reopen.
5. **Stop caching errors you did not intend to cache.** RFC 9111 lists 404, 405, 410, 414 and 501 among heuristically cacheable status codes. Cache 404 and 410 for 10 to 30 seconds at most, never cache 400 or 5xx, and align the CDN's maximum header size with the origin's so oversized requests are rejected at the edge. Why: this closes CPDoS.
6. **Normalize methods and bodies.** Strip X-HTTP-Method-Override, X-HTTP-Method and X-Method-Override at the edge, and drop bodies on GET and HEAD before forwarding. Why: the cache key assumes the method it sees is the method the origin executes.
7. **Purge and re-probe.** After the rules ship, purge affected paths and repeat step 3 for every input. Why: responses poisoned before the fix stay valid until purge or expiry.

| Input type | Rule | Hit ratio cost | Example |
| --- | --- | --- | --- |
| Client-supplied proxy headers the origin should never trust | Strip at edge, overwrite with edge-set values | None | X-Forwarded-Host, X-Original-URL, X-Rewrite-URL |
| Inputs that legitimately vary output with few values | Add to cache key, normalized to an allowlist | Divides traffic per key by the number of variants | Accept-Encoding mapped to br, zstd, gzip or identity |
| Method or body overrides | Strip, or reject with a non-cached 400 | None | X-HTTP-Method-Override, GET bodies |
| Dropped tracking parameters the origin reflects | Strip before forwarding, not just from the key | None | utm\_source echoed into a canonical link |
| High-cardinality inputs that vary output | Mark the route uncacheable or move variation client-side | Route leaves the cache entirely | Raw User-Agent, session cookies |

Stripping is the default rule because it closes the vector at zero hit ratio cost; keying is reserved for inputs with a small, normalized set of legitimate values.

The key rule is where trade-offs bite. Keying Accept-Language raw, with dozens of distinct strings in the wild, can split one cached response into 40 or more variants and multiply origin fetches per TTL by the same factor. Normalize to the locales you actually serve, typically 3 to 10, before adding anything to the key. Relying on Vary alone is weaker: CDN support for arbitrary Vary values is uneven, and a Vary on User-Agent fragments the cache to near zero.

If you deliver heavy static traffic through BlazingCDN, custom configurations and cache rules are available and each zone is set up together with an engineer, so the strip and key rules from this matrix can be written into the zone at onboarding, with purge through the API for step 7. The [**BlazingCDN features covering cache rules and purge**](https://blazingcdn.com/features/) list what is included. BlazingCDN is a delivery layer, not a WAF, so keep your existing security stack for request filtering.

## **How to validate the cache key fix**

Re-run the canary probe from step 3 for every input in your inventory, against at least the 20 highest-traffic cacheable routes. Expected result: the clean follow-up request never contains the canary, regardless of cache status. For error caching, send a request with a header past the origin limit to a unique path, then a clean request; the clean request must return 200 from origin, not a cached 400.

Then watch two metrics for 24 hours. Cache hit ratio per route should stay within 1 to 2 points of its baseline after stripping; a larger drop means you keyed something with too many values. Origin request rate should not climb by more than the variant count you deliberately introduced.

## **Rollback**

Strip rules are low risk to roll back but should rarely need it. If one breaks a feature, the origin was depending on a client-supplied header, which is the vulnerability itself, so fix the origin to read an edge-set header instead. Key changes are the risky ones: ship them one route group at a time, and revert by removing the key component and purging that group.

## **Common failure modes: symptom, cause, fix**

| Symptom | Cause | Fix |
| --- | --- | --- |
| Redirect loops after stripping X-Forwarded-Proto | Origin can no longer tell the edge used HTTPS | Have the edge set the header itself, overwriting any client value |
| Canary still appears after the fix | An intermediate cache (origin shield, framework cache) has its own key | Audit every cache tier, then purge each one |
| Hit ratio drops 10 points or more | A raw, high-cardinality header was added to the key | Normalize to an allowlist before keying |
| Intermittent site-wide 400s | Negative caching of client errors | Disable caching for 400 and 5xx, cap 404 at 10 to 30 seconds |

Most post-fix surprises come from a second cache tier with its own key, so audit the shield and any framework cache with the same rigor as the edge.

## **Tuning once the cache key is clean**

Add a scheduled canary probe to CI that runs the step 3 check against new routes on every deploy, since framework upgrades frequently change trusted-proxy defaults. Log the full set of forwarded headers for a 1 percent sample of origin requests and alert when a new header appears that is not on your inventory. Finally, revisit TTLs: once inputs are controlled, longer HTML TTLs become safe and recover the hit ratio you may have traded away.

## **FAQ: web cache poisoning and cache key design**

### What is an unkeyed header in web cache poisoning?

An unkeyed header is a request header the origin uses to build a response but the cache does not include in its cache key. Because the cache treats requests with different values as identical, a response generated from an attacker's value is stored and served to every user requesting the same URL until the TTL expires or the path is purged.

### Does adding Vary headers prevent web cache poisoning?

Vary helps only partially and often hurts performance. It tells caches to key on listed headers, but CDN support for arbitrary Vary values is inconsistent, and varying on high-cardinality headers such as User-Agent fragments the cache badly. Stripping untrusted headers at the edge, and keying only normalized, low-cardinality inputs, is more reliable than depending on Vary.

### Can cache poisoning cause a denial of service?

Yes, cache-poisoned denial of service happens when an error response gets cached for a popular URL. An attacker sends a request the origin rejects, for example an oversized header or a method override, and if the CDN stores that 400 or 405, every user receives the error until expiry. Never cache 400 or 5xx responses.

### How do I test my CDN cache key for poisoning safely?

Test against unique cache-buster paths so no real user shares the poisoned cache key. Send one request with a canary value in the suspect header, then a clean request to the same unique path. If the clean response contains the canary and shows a cache hit, that input is poisonable and needs to be stripped or keyed.

## **Run the canary audit on your top 20 cacheable routes this week**

Pick the 20 highest-traffic cacheable routes from your CDN logs and run the two-request canary probe against each, covering X-Forwarded-Host, X-Forwarded-Scheme, X-Original-URL, method override headers and every query parameter your key drops. Record which inputs reflect, which get stored, and which status codes your edge caches today. That one table tells you whether you need strip rules, key normalization or error caching changes, and it takes about 90 minutes per zone whether or not you change providers.

For high-volume static delivery, the [**BlazingCDN Anycast CDN with custom cache rules**](https://blazingcdn.com/anycast-cdn/) zones are configured with an engineer during roughly one hour of onboarding, so the key and strip rules from your audit can be in place from day one.

[View full post](https://blog.blazingcdn.com/en-us/web-cache-poisoning-how-it-happens-and-how-to-prevent-it)

```json
{
  "@context" : "http://schema.org",
  "@type" : "BlogPosting",
  "author" : {
    "@type" : "Person",
    "name" : "Vadym Borovskyi"
  },
  "dateModified" : "2026-11-01T21:32:45.086Z",
  "datePublished" : "2026-11-01T21:32:44Z",
  "headline" : "Web Cache Poisoning: How It Happens and How to Prevent It",
  "image" : {
    "@type" : "ImageObject",
    "height" : 768,
    "url" : "https://143144902.fs1.hubspotusercontent-eu1.net/hubfs/143144902/Gemini-Blog/image-Nov-01-2026-09-30-35-7712-PM.jpeg",
    "width" : 1376
  },
  "mainEntityOfPage" : "https://blog.blazingcdn.com/en-us/web-cache-poisoning-how-it-happens-and-how-to-prevent-it",
  "publisher" : {
    "@type" : "Organization",
    "logo" : {
      "@type" : "ImageObject",
      "height" : 60.0,
      "url" : "https://143144902.fs1.hubspotusercontent-eu1.net/hubfs/143144902/Logo%20b-3.png",
      "width" : 245.85367
    },
    "name" : "CDN Blog"
  }
}
```