# Cache Poisoning at Scale

Survey of 70+ cache poisoning bugs across CDNs: unkeyed headers, URL fragment handling in ATS, host-header bypasses, and keyed-parameter tricks caused DoS, redirects, and stored XSS.

- Payout: unconfirmed
- Severity: high
- Program: GitHub, GitLab, HackerOne, Shopify, Cloudflare
- Credited: Youstin
- Disclosed: 2021-12-23
- CVE: CVE-2021-27577
- Write-up quality: 9/10
- Original: https://youst.in/posts/cache-poisoning-at-scale/
- Library page: https://bugbountylibrary.com/r/blog%3Ayoust-in-posts-cache-poisoning-at-scale
- Tags: domain:web impact:dos kind:bounty platform:blog sev:high tech:cdn technique:param-pollution technique:recon vuln:cache-deception vuln:cache-poisoning vuln:dos vuln:host-header vuln:xss

**Target:** CDN/cache layers (Cloudflare, Fastly/Varnish, Apache Traffic Server, Akamai) and origin backends
**Root cause:** Caches build keys from a subset of request components (host/path/query or selected params) while forwarding other components (fragments, unkeyed headers, duplicate params, illegal headers) to backends that react differently, letting attackers store attacker-chosen responses under victim keys.
**Steps:**
1. ATS: send `GET /?x#/../evil HTTP/1.1` with `Host: target`; ATS keys on path+query but forwards fragment; backend encodes `#` to `%23` and normalizes `/../`, caching a different path under the victim key.
2. GitHub: send request with invalid `Content-Type` value to a repo URL; unauthenticated cache key poisoned, repo DoS for all anonymous visitors.
3. GitLab: `GET https://assets.gitlab-static.net/...` with `x-http-method-override: HEAD` (or `PURGE`) to cache empty body / 405.
4. Rack: send `x-forwarded-scheme: http` to trigger 301 redirect to same location; cached redirect loop DoS.
5. Cloudflare+S3: send invalid `Authorization` header to get cacheable 403 (pre-Aug 2021 default).
6. Fastly host bypass: send `Host: attacker` plus `Fastly-host: <whitelisted>` to satisfy Fastly whitelist while backend uses Host; combined with coordinate-only cache key regex `/4/151/16` to poison arbitrary host content.
7. Keyed param: `?size=100&size%3d0` — encoded second param ignored by cache key but used by backend, yielding cacheable 400.
8. Akamai: send header name containing `\` to get cacheable 400.
9. Scanner UA: send `User-Agent: ffuf`/`nuclei` to trigger blocked/cacheable error response.
**How found:** Started from a Varnish capitalized-Host edge case; noticed Cloudflare lowercased Host for the cache key but forwarded it verbatim, generalizing to a hunt for cache-key/forwarding mismatches. Built a header-bruteforce scanner for unkeyed headers, ran it at scale on bounty scopes, then manually investigated anomalies (e.g., GitHub repos flagged but not reproducible in browser → discovered Auth cookie in cache key). Later obtained a Varnish VCL to read custom key rules, and queried HTTP Archive BigQuery for `Vary` header values to expand the header wordlist.
**Exploitation:** Attacker sends a single crafted request (often unauthenticated) that the CDN caches under a key shared by legitimate users; subsequent victims receive the poisoned response — empty body (DoS), 301 redirect loop, redirect to attacker JS (stored XSS across 21 subdomains via X-Forwarded-Host), or attacker-hosted HTML served under victim origin via Fastly-host bypass.
**Escalation:** Chained host-header bypass with DOM XSS on a CDN host to move the vulnerable file onto the main assets domain, achieving XSS under a trusted origin; ATS fragment trick escalated to open redirect/XSS when backend normalized `/../`.
**Insight:** Hunt for divergence between what the cache keys on and what the origin consumes: duplicate/encoded params where the cache keys the first and backend uses the last, URL fragments forwarded but not keyed, and CDN-specific whitelist headers (e.g., Fastly-host) that let you override Host while still passing validation. Mine HTTP Archive `Vary` data for real-world unkeyed header candidates.
