Cache poisoning & deception
Cache-buster isolation
Section titled “Cache-buster isolation”Unkeyed input mining, web cache entanglement, error response caching, response splitting, and CPDoS each write attacker-controlled content or an error response back through the resource’s own URL, then send a follow-up request to confirm a shared cache is replaying it. Both requests in that pair go to a cache-buster-isolated URL/header — a fresh random token per attempt (--cache-buster-param, default cd_cachebuster, as a query param by default; --cache-buster-mode header/both for a header instead) — instead of the resource’s plain canonical URL, so confirming the vulnerability never poisons the cache entry a real visitor’s plain request would read from. See scan’s cache-buster flags.
Unkeyed input mining
Section titled “Unkeyed input mining”Wordlist-driven mining across three input surfaces — headers, cookies, and query parameters — for candidates a fronting cache leaves out of its cache key: injects a marker via each candidate (isolated behind a fresh cache-buster token) and sends a follow-up plain request to the same isolated URL/header to check whether it receives the poisoned response back. This is the classic web cache poisoning precondition — Kettle’s “Practical Web Cache Poisoning” (2018) and the Param Miner discovery methodology.
Each surface mines its own bundled default wordlist, in this order:
- Headers —
X-Forwarded-Host,X-Forwarded-Scheme,X-Forwarded-Proto,X-Forwarded-Server,X-Host,X-Original-URL,X-Rewrite-URL - Cookies —
debug,beta,preview,test,lang,language,locale,region,country,currency,theme,mobile - Query parameters —
debug,test,preview,callback,jsonp,redirect,return_url,next,utm_source,utm_campaign,ref,lang,locale
scan’s --unkeyed-wordlist supplements every surface’s bundled list with app-specific candidate names of your own. --max-aggressive-requests is shared across all three surfaces (headers exhausted before cookies before query parameters), so mining deep into the bundled — let alone a custom — wordlist may need raising it well past its default of 10.
| Finding | Severity | CWE |
|---|---|---|
Unkeyed header injection via <header> | critical | CWE-444 |
Unkeyed cookie injection via <cookie> | critical | CWE-444 |
Unkeyed query parameter injection via <param> | critical | CWE-444 |
Web cache entanglement (fat GET & parameter cloaking)
Section titled “Web cache entanglement (fat GET & parameter cloaking)”Two more ways an origin can read an input the cache key ignores, both from Kettle’s “Web Cache Entanglement: Novel Pathways to Poisoning” (Black Hat USA 2020). Both use the same marker-and-confirm method as unkeyed input mining, on a cache-buster-isolated URL.
- Fat GET — a
GETrequest carrying a body. Caches key on the URL and don’t expect a GET body to change the response, but some origins still let it override a query parameter. The marker is sent as a raw body, then as form-encoded and JSON bodies keyed by the resource’s own query parameter names (orqwhen it has none). Skipped when the scan method isn’tGET, since a body is ordinary there. - Parameter cloaking —
;<name>=<marker>appended to the query string. A cache that stops reading the query at the first;keys the request like the plain URL, while an origin that treats;as a parameter separator (Rails, some Java stacks) still reads the cloaked parameter. Candidate names are the resource’s own query parameters, thenname,q,search,query,callback,redirect,url,lang, plus any--unkeyed-wordlistentries.
This check has its own --max-aggressive-requests budget (it isn’t shared with unkeyed input mining), so both techniques run within the default of 10: fat GET first, then cloaking.
| Finding | Severity | CWE |
|---|---|---|
| Cache poisoning via fat GET request body | critical | CWE-436 |
Cache poisoning via parameter cloaking (<name>) | critical | CWE-436 |
Cache deception
Section titled “Cache deception”Appends a static-file extension or a nonexistent sub-path (/nonexistent.js, .css, /nonexistent.jpg, …) to the resource’s path and checks whether the (potentially authenticated) origin response is served — and looks cacheable — under that URL. This is cache deception via path confusion / static-extension probing.
The static extensions probed come from the CDN registry’s StaticExtensions field (cdn.AllStaticExtensions) — every extension any registered CDN caches by default, deduplicated. The check also tries a handful of origin-only path delimiters (;, %00, %0a, $) ahead of the plain suffixes: some caches decide cacheability from whatever static-looking suffix follows one of these characters, while the origin ignores the delimiter and keeps routing on the full path — a distinct mechanism from a bare static extension, documented by PortSwigger’s “Gotta cache ‘em all” research. For a systematic sweep of which delimiters a target’s origin and cache disagree on, see Delimiter & normalization mapping below.
| Finding | Severity | CWE |
|---|---|---|
| Cache deception via path confusion | high | CWE-524 |
Delimiter & normalization mapping
Section titled “Delimiter & normalization mapping”Maps the characters the origin treats as a path delimiter — truncating its route there — but a fronting cache keys on verbatim, and the reverse: characters the cache truncates its key at while the origin routes on the full path. That split is the foundation of both cache-key poisoning and cache deception: the cache stores the origin’s real (possibly authenticated) response under a distinct, attacker-chosen URL, or stores an attacker-provoked response under the real resource’s key. Based on Doyhenard’s “Gotta cache ‘em all” (Black Hat USA 2024).
Each candidate uses the baseline/suffix/insert probe:
- R0 — the undelimited baseline response for the resource path.
- R1 — the path plus a random, unrouteable suffix (no delimiter). If R1 already matches R0, the origin ignores trailing path junk wholesale, so no delimiter’s effect can be isolated and the sweep is skipped.
- R2 — the path plus a candidate delimiter plus that suffix. If R2 matches R0, the origin truncated its route at the delimiter while a cache keying on the full request path would store that response under the distinct
…<delimiter><suffix>URL — an origin-only delimiter. R1 and R2 each carry their own cache-buster, so they are answered by the origin’s routing of that exact path — otherwise a cache that truncates its key at the delimiter would replay the cached baseline and be mistaken for an origin-only delimiter.
For a candidate where R2 differs from R0 the check tests the reverse direction on a cache-buster-isolated entry: it requests <path>?<buster> + delimiter + suffix (the origin answers with something other than R0, typically a 404), then the plain <path>?<buster>. If that second, undelimited request is served the first one’s response, the cache keyed it under the undelimited key — it truncates at the delimiter — a cache-only delimiter. Any response the origin can be made to return for the delimited URL then replaces the real resource for every visitor (cache poisoning / denial of service). This costs two extra requests per candidate and runs after the other probes.
The candidates probed, each sent on the wire exactly as written (the percent-encoded forms reach the origin as those literal bytes, not a decoded octet): ;, ., $, %00, %0a, %23, %5c. A bare # from the research’s candidate list is omitted deliberately — a spec-compliant client can’t send an unencoded fragment delimiter in the path (it never leaves the client), so %23 covers the only transmittable form.
The check also probes path-normalization order with dot-segment traversal, encoded (..%2f) and plain (../): it inserts a nonexistent /<rand>/../ detour that collapses back to the resource only if the server normalizes the dot-segment. An origin that normalizes while the fronting cache keys on the verbatim, un-normalized detour path is the “normalize before vs. after cache rules” discrepancy — CloudFront/Azure/Imperva normalize before applying cache rules (not affected), while Cloudflare/Google Cloud/Fastly do not (the cache stores the origin’s response under the detour URL). The encoded form is the higher-risk one; a plain ../ is standard RFC 3986 behavior most caches share, so the origin result alone is reported for mapping only.
For each form the origin normalizes, the check then measures the cache side on a cache-buster-isolated entry. It primes <path>?<buster> and requires the repeat to be served from cache (a normalized HIT, or a non-zero Age) — otherwise the cache isn’t observably storing the resource and the order can’t be told apart from a miss. Then it requests the detour with a fresh random segment and the same buster:
- served from cache straight away → the detour collapsed onto the primed canonical key: the cache normalizes before keying (info — it agrees with the origin);
- a miss, then served from cache on a repeat → the cache stored the origin’s response under the verbatim detour URL: it normalizes after (or never) — the web-cache-deception condition (high for the encoded form, medium for the plain one);
- neither → not determined, and nothing is recorded.
The measured order is compared with the PathNormalization the CDN registry documents for the detected CDN, and the finding notes whether they agree — a customer’s rule set or an extra cache tier can override the vendor default. If the order can’t be measured but the CDN is fingerprinted with documented behavior, an info finding states that unverified expectation. The probe costs up to six requests (two to prime, up to two per form).
Per-origin results (the origin-only and cache-only delimiters found, the origin’s normalization verdicts, and the measured cache-side order for each form) are recorded in the CDN quirk store (cdn.DiscoveredQuirks), accumulated across every resource probed under a target so a later resource — or an embedding tool — can reuse the mapping instead of re-probing.
The baseline R0 is fetched at the resource’s own URL, so the check runs after the rest of the §5 chain to avoid that request racing another check’s poison/confirm pair; every request that could write to a cache is cache-buster-isolated. Full coverage needs a request budget well above the default: up to 30 requests per resource (the origin-side sweep takes 10; the rest go to the cache-side probes in order — normalization order, then cache-only delimiters), so raise --max-aggressive-requests to cover them.
| Finding | Severity | CWE |
|---|---|---|
Origin-only path delimiter <delimiter> | high (cacheable) / medium | CWE-524 |
| Origin normalizes encoded path traversal | high | CWE-524 |
| Origin normalizes plain path traversal | info | CWE-524 |
Cache-only path delimiter <delimiter> | high | CWE-524 |
Cache keys encoded ..%2f traversal verbatim | high | CWE-524 |
Cache keys plain ../ traversal verbatim | medium | CWE-524 |
| Cache normalizes encoded/plain traversal before applying cache rules | info | CWE-524 |
| Cache path-normalization order not observable | info | CWE-524 |
Static-directory path-traversal deception
Section titled “Static-directory path-traversal deception”Requests the resource through a path that starts under a static directory and climbs back out — /static/<rand>/../../account — and checks whether the origin still serves the resource and the response then gets cached. A cache that applies its “cache everything under /static/” rule to the raw path caches the origin’s dynamic response under a URL anyone can request. Three spellings are tried, because caches and origins disagree on when they decode:
| Variant | Path probed | Catches |
|---|---|---|
| plain | /static/x/../../account | cache matches its directory rule before resolving ../ |
| encoded | /static/x/..%2f..%2faccount | cache matches the rule before decoding %2f |
| nested-encoded | /static/x/..%252f..%252faccount | cache decodes once, origin keeps decoding until the separators are real |
Directories tried, in order: /static, /assets, /public, /images, /css, /js, /media. Each variant stops at its first hit. With the default --max-aggressive-requests of 10, /static and /assets are covered for all three variants; raise the cap to reach the rest.
Matching the canonical response only shows the origin normalizes the path, which most do. So the check also:
- sends a control request with no traversal (
/static/x/account) and gives up if the origin serves the resource for that too — it routes loosely, and a match can’t be attributed to normalization; - skips resources whose canonical URL is already a cache
HIT, since caching one more URL for public content exposes nothing new; - repeats the matching request and looks for a cache
HIT. AHITon the traversal URL while the canonical URL isn’t cached is the deception working (high). With no readable cache-status header the finding is medium and says caching wasn’t observed.
| Finding | Severity | CWE |
|---|---|---|
| Cache deception via static-directory path traversal | high (cache HIT observed) / medium | CWE-524 |
| Cache deception via encoded static-directory path traversal | high / medium | CWE-524 |
| Cache deception via nested-encoded static-directory path traversal | high / medium | CWE-524 |
Cache-key path collision
Section titled “Cache-key path collision”The poisoning counterpart of the check above, also from “Gotta cache ‘em all”: a cache that normalizes ../ before computing its key while the origin routes the raw path. /static/x/../../account is then served by the origin as static content but stored under the key /account — one request overwrites any other path’s cache entry (“cache what/where”).
The probe never touches a real entry. It collides onto a random, never-requested path: request /static/<seg>/../../<rand>, and if the origin answered with content that differs from what a nonexistent path returns, request /<rand> plainly. Getting the static content back from a path nothing ever served proves the cache keyed the first request as /<rand>. A catch-all origin (every path answers alike), or one that normalizes the path itself, produces no finding.
Plain and %2f-encoded separators are tried against /static and /assets; the first confirmed collision ends the check. It needs a static route that returns 2xx for an arbitrary filename under the directory, so an origin whose static handler 404s on unknown files can’t be probed this way and is not reported.
| Finding | Severity | CWE |
|---|---|---|
| Cache-key path collision via path traversal | critical | CWE-436 |
Error response caching
Section titled “Error response caching”Provokes a 4xx/5xx response (via an out-of-range Range request) and checks whether the error response itself carries cache directives that make it cacheable — a transient error served to other users for longer than intended.
| Finding | Severity | CWE |
|---|---|---|
| Error response is cacheable | medium | CWE-524 |
Response splitting via cache-key manipulation
Section titled “Response splitting via cache-key manipulation”Injects a CRLF-encoded header/path fragment into an unkeyed input and checks whether it’s reflected as a literal, separate response header — HTTP response splitting exploitable through the cache.
| Finding | Severity | CWE |
|---|---|---|
| Response splitting via unkeyed input | critical | CWE-113 |
This check depends on the unkeyed-header-injection check completing first (rather than just on resource discovery) — both manipulate the same unkeyed inputs on the same live resource, and running them concurrently could let one check’s poisoning request land in the middle of the other’s poison/confirm pair and mask its finding.
Cache-Poisoned Denial of Service (CPDoS)
Section titled “Cache-Poisoned Denial of Service (CPDoS)”Reproduces the three variants from the original 2019 CPDoS disclosure (see the CDN registry’s cpdos-2019 Quirk) against the live target, each as a poison-then-confirm pair: provoke an origin error, then check whether a follow-up plain request is served that same error back — meaning it was cached and is now being replayed to ordinary callers, not a one-off response to the probe itself.
- HTTP Header Oversize (HHO) — an oversized header block a fronting cache accepts but the origin’s own, smaller limit rejects.
- HTTP Method Override (HMO) — an
X-HTTP-Method-Override: DELETEheader the origin honors (re-interpreting the request) but the cache ignores, keying purely on the realGETline. - HTTP Meta Character (HMC) — a control character injected into an existing query parameter’s value, rejected by the origin’s input validation while the cache keys on the path alone. Needs a resource with at least one query parameter to mutate; a bare path has no surface for this variant and is silently skipped.
Stops after the first confirmed variant per resource: once one has poisoned a (typically single-slot) cache, the remaining variants would just observe that same residual poisoning rather than an independent finding.
| Finding | Severity | CWE |
|---|---|---|
| CPDoS via oversized headers (HHO) | high | CWE-444 |
| CPDoS via method override (HMO) | high | CWE-444 |
| CPDoS via HTTP meta-character (HMC) | high | CWE-444 |
Cache-key explosion
Section titled “Cache-key explosion”Sends the resource 20 times, each with a different random value for one query parameter (its first existing one, or cd_key), and checks whether every value became its own cache entry — a MISS each time, and a HIT on repeating the first. If nothing ever caps the number of distinct entries, an attacker can fill the cache with junk and force an origin request per unique value, defeating the cache.
Treat this as an exposure assessment, not a defect: most caches key on the full query string by design, so it reports at low severity and the fix is edge-side (rate limiting, query-parameter allowlisting or normalization). Two constraints keep it from guessing:
- It needs
--max-aggressive-requestsof at least 21 (20 distinct values plus one repeat) and a readable cache-status header. Below that there is no way to tell “no cap” from “a cap above the sample”, so the check sends nothing at the default of10. - Any
HITamong the 20 means a cap, a shared overflow bucket, or a parameter the cache doesn’t key on, and ends the check without a finding.
It leaves 20 junk entries in the target’s cache, which is why it sits behind --aggressive with the rest of this page.
| Finding | Severity | CWE |
|---|---|---|
| Unbounded cache-key cardinality | low | CWE-400 |