Web Cache Poisoning: Defence Patterns That Actually Hold
Cybersecurity
Cache poisoning via header injection still ships in production CDNs and origin caches. The fixes are cache key normalization, header allowlisting, and busting on critical paths. None of them happen by default.
By Arjun Raghavan, Security & Systems Lead, BIPI · September 22, 2024 · 7 min read
We poisoned the cache of a SaaS dashboard during a pentest by setting X-Forwarded-Host on a request to a static asset. The application reflected the value into a redirect URL on the page. The CDN cached the response, keyed on path only. Every subsequent visitor to that asset got a redirect to our server. The cache TTL was four hours.
The customer's response was 'but X-Forwarded-Host is internal'. It was set by their load balancer for legitimate proxy purposes. The CDN didn't strip it. The application trusted it. Standard chain: the issue wasn't any single component, it was the assumptions between components.
How cache poisoning works
A web cache stores responses keyed on some subset of the request, typically the URL plus a few headers (Vary). Cache poisoning happens when an attacker can include something in the request that affects the cached response but is not part of the cache key. The next person who fetches the same key gets the attacker-influenced response.
The most common vector is unkeyed input. Headers like X-Forwarded-Host, X-Forwarded-For, X-Original-URL, User-Agent, Accept-Language: any header the application reads but the cache doesn't include in the key. If the application reflects, redirects, or branches on that header, you have a poisoning primitive.
The major CDN bugs of 2023-2024
Akamai had a Pragma-based poisoning bug disclosed in 2023 where staging-grade headers could trigger cached error pages. Cloudflare had a CF-Connecting-IP normalization bug exploitable in specific worker configurations. Fastly had cache key issues with header capitalisation in specific surrogate-key flows. None of these were the customer's fault. All of them required customer configuration to mitigate.
The pattern: CDN ships a feature, customers don't fully understand the cache key, attacker finds a header the app reads but the CDN doesn't key, exploitation follows. The CDN patches eventually. In the gap, every customer using the affected feature is exposed.
Defence patterns
Header allowlist at the CDN. Strip everything that isn't on the list before the request reaches origin. The list is short: standard request headers, auth headers, business-critical custom headers. Anything else gets dropped. This kills most poisoning vectors because attackers can't reach the application with the exotic headers needed for the exploit.
Cache key normalisation. Be explicit about what's in the key and what isn't. If you Vary on User-Agent, normalise User-Agent to a small set of buckets first (mobile/desktop/bot) so attackers can't fragment the cache. If you key on a subset of query params, document which ones and reject requests with extras.
- Strip X-Forwarded-* headers at the CDN unless explicitly required
- Normalise headers before keying: lowercase, strip whitespace, bucket values
- Set Cache-Control: no-store on any path that returns user-specific or auth-required data
- Use cache-busting query params on critical paths (login, password reset, payment)
- Set short TTLs on dynamic content; reserve long TTLs for true static assets
Detection in production
Cache poisoning rarely sets off application alerts. The application sees a normal request, returns a normal response, and goes back to sleep. The poisoned response sits in cache silently until the TTL expires.
Detection requires comparing what the cache served to what origin would have served. A canary request: poll critical pages every minute, compare the cached response to a fresh origin response, alert on diffs. We deploy this on the top 50 pages by traffic for clients with serious cache risk. It catches poisoning within minutes and triggers a cache purge runbook.
Where to start
- Inventory headers your application reads: grep for getHeader, request.headers, etc.
- For each header, confirm it's part of the cache key or stripped at the CDN
- List paths with long cache TTLs and high traffic; treat them as poisoning targets
- Test with PortSwigger's Param Miner extension against staging
- Add canary monitoring on critical cached paths
Cache poisoning is a perfect class of bug for security investment: well-understood mechanics, durable defences, automated detection, and high impact when missed. The teams that get it right do header allowlisting and cache key normalisation as a one-time project. The teams that don't keep finding new variants in their incident reviews.
Read more field notes, explore our services, or get in touch at info@bipi.in. Privacy Policy · Terms.