An unkeyed header that influences a cached response is reachable. One that stores your injected value and serves it cold to a separate session is confirmed - and the difference is a cache buster.
Web cache poisoning turns an input the cache ignores into a delivery mechanism it cannot help but store. The cache keys a response on the URL and a fixed set of headers; the application quietly reads one that is not in that set. Reflect an attacker value through that unkeyed input and the cache saves it, then serves it - cold - to everyone who requests the same key. Confirming that without poisoning a single real visitor is a matter of scoping the blast radius to a key nobody else will ask for.
The cache key is usually the path plus a short header allowlist. Anything outside that list - a stray X-Forwarded-Host, X-Forwarded-Scheme, or a custom header the app trusts - is unkeyed: it can change the response body but not the key the response is stored under. That gap is the whole vulnerability.
Confirming stored impact means demonstrating a cold serve - a second request, without the injected header, receiving the injected value. Doing that on the live key would poison real users. So the probe adds a unique query parameter, ?cb=<nonce>, which most layers include in the key. The poisoned entry now lives under a key no real user will ever type, and the confirmation is self-contained.
# 1) inject through the unkeyed header, scoped to a private key curl -s "https://site/home?cb=ap_62615533" \ -H "X-Forwarded-Host: ap-probe.example" -o /dev/null # 2) fetch the SAME key with NO injected header - a real user's request shape curl -s "https://site/home?cb=ap_62615533" | grep ap-probe.example # a match here == the value was stored and served cold == confirmed
A probe that only checks for X-Cache: HIT works against one CDN and goes quiet against the next. Default key sets and header normalization differ enough that the engine treats the layer as a variable, not a constant.
| Layer | Default key behaviour | What shifts the probe |
|---|---|---|
| Nginx | Explicit proxy_cache_key; often path plus Host | Depends entirely on operator config |
| Varnish | VCL-defined; hash_data controls the key | Custom vary logic can key or unkey any header |
| Cloudflare | URL-based by default, query included | Cache rules and header transforms change the surface |
| Fastly | VCL; normalizes and can vary aggressively | Header normalization can hide or expose the input |
Confirming a cold serve is the reachability-to-impact step; the number comes from the sink. If the unkeyed host lands in a JavaScript import path, the cached entry is stored XSS served to every visitor - critical. If it only changes a canonical link or a redirect target, it is lower. Same mechanism, different rating, always tied to the confirmed sink.
Add the header to the cache key, or stop reading it in the app, and the ride-along input has nowhere to hide. Until then, the proof is a nonce and a second request.