Shared caches in CDNs and reverse proxies serve one stored response to many users, so a mistake about what is stored, or under which key, becomes a mistake for everyone. Web cache poisoning makes a cache store a harmful response for a legitimate URL. Web cache deception does the opposite: it tricks a cache into storing a victim's private, authenticated page where an attacker can then retrieve it. Both come from the same design gap: the cache and the origin disagree about what makes two requests the same, or about what may be cached. This guide explains both at concept level, where they appear, how to detect them and how to design cache keys and caching rules that stay safe. It also covers the build-pipeline variant, CI cache poisoning, briefly. It is for application security engineers, platform and CDN owners, and developers who set response headers. It contains no request examples or payloads.
Key takeaways
- Poisoning and deception are mirror images. Poisoning pushes attacker content to many users; deception pulls one user's private content out to the attacker.
- Unkeyed inputs are the poisoning risk. Any header, cookie or parameter that changes the response but is not in the cache key lets one request decide what everyone receives.
- Path confusion drives deception. Caches that decide by file extension or path pattern, in front of origins that ignore or normalise extra path segments, will store dynamic pages as if they were static files.
- Authenticated responses must say so.
Cache-Control: no-storeorprivateon every personalised response is the single most effective control, backed by RFC 9111 rules shared caches must follow. - Build caches are caches too. The Ultralytics compromise shows that shared CI caches can carry poisoned content into release builds.
How the attack works
RFC 9111 defines the cache key as composed, at a minimum, of the request method and the target URI, and allows a cache to add more material. In practice CDNs and proxies build a key from scheme, host, path and some or all of the query string, and add headers only when told to (by configuration or by a Vary response header). Everything else in the request is unkeyed: it may reach the origin, but the cache ignores it when deciding whether two requests match.
Web cache poisoning
- Mechanism: the origin uses an unkeyed input (for example a forwarding header, a cookie or an ignored query parameter) to build part of the response, such as an absolute URL, a redirect target or a script source. A request with a crafted value produces a harmful response, the cache stores it under the normal key, and later visitors receive it.
- Typical outcomes: stored cross-site scripting, redirects to attacker sites, broken pages (denial of service by caching an error), or resources loaded from an attacker host.
- Related paths: HTTP request smuggling can also poison caches, which CWE-444 lists as a consequence and CISA's description of CVE-2022-22536 mentions. DNS cache poisoning (CAPEC-142) is a different attack on resolvers. See the DNS Security Guide.
Web cache deception
- Mechanism: the attacker persuades a logged-in victim to open a URL that looks like a static file to the cache but that the origin treats as a request for a dynamic page, for instance because the origin ignores trailing path segments or interprets delimiters and encoded characters differently. The origin returns the victim's account page; the cache stores it because the path looks cacheable; the attacker then requests the same URL and receives the stored copy.
- Preconditions: a cache rule based on extension or path pattern, an origin that maps unexpected paths to dynamic content instead of returning 404, and a personalised response that does not forbid caching.
Cloudflare's documentation describes exactly this pattern: a dynamic page served for a made-up path ending in an image extension, cached because of the extension. MITRE catalogues the general pattern as CAPEC-141, Cache Poisoning, and the exposure of stored sensitive data as CWE-524, Use of Cache Containing Sensitive Information.
| Aspect | Web cache poisoning | Web cache deception |
|---|---|---|
| Direction | Attacker content to many users | One victim's data to the attacker |
| Root cause | Response varies on an unkeyed input | Cache and origin disagree on whether a path is static |
| Victim action needed | None beyond normal browsing | Victim must open a crafted link while logged in |
| Typical impact | Stored XSS, redirects, defacement, denial of service | Exposure of personal data, CSRF tokens, API keys, session details |
| Primary fix | Key every input that changes the response, or stop the origin using it | Mark personalised responses non-cacheable; return 404 for unknown paths; match content type to extension |
Where it shows up
| Component | Default behaviour to know | Risk if changed carelessly |
|---|---|---|
| Cloudflare | Caches by file extension, not MIME type; does not cache HTML or JSON by default; does not cache responses with Set-Cookie or with private, no-store, no-cache or max-age=0 | "Cache everything" style rules on dynamic paths, or edge TTL rules that override origin headers |
| Amazon CloudFront | A cache policy sets which headers, cookies and query strings are in the cache key; an origin request policy forwards values to the origin without adding them to the key | Values forwarded by an origin request policy but used by the origin to build responses are unkeyed inputs by design |
| NGINX proxy cache | proxy_cache_key defaults to $scheme$proxy_host$request_uri; responses with Set-Cookie are not cached; Vary: * is not cached | proxy_ignore_headers Set-Cookie or ignoring Cache-Control lets session-bearing responses into the shared cache |
| Application frameworks | Often trust forwarding headers to build absolute URLs and redirects | Trusting forwarding headers from any client makes them unkeyed, attacker-controlled inputs |
| CI caches (for example GitHub Actions) | Runs restore caches from their own branch, the default branch and, for pull requests, the base branch; contents are not signed or verified | Release jobs restoring caches that lower-trust jobs could write |
Multi-layer setups (CDN, then a regional proxy cache, then an application cache) multiply the risk because each layer has its own key and rules. Edge configuration is often owned by a different team from the application, so changes to caching rules are a frequent trigger.
Impact
- Mass client-side compromise: a poisoned script or page is served to every visitor of a popular URL until the entry expires or is purged. RFC 9111 Section 7.1 notes that storing malicious content in a cache extends an attacker's reach to multiple users.
- Account data exposure: deception exposes whatever the personalised page contains, which can include names, addresses, order history, CSRF tokens and API keys shown in account settings.
- Credential and token leakage: RFC 9111 Section 7.3 warns that implementation or deployment flaws can lead to caching of authentication credentials. A cached response carrying a session cookie or bearer token hands that identity to anyone who receives it. See the Session Hijacking and Cookie Theft Guide.
- Availability: caching an error or redirect for a key page takes it offline for all users.
- Supply chain: a poisoned build cache can put attacker code into signed, published artefacts that downstream users trust.
Detection
- Cache status on sensitive paths: log the cache result (hit, miss, bypass) at the CDN and alert on any hit for authenticated paths such as account, profile, settings, checkout and API routes. There should be none.
- Content type mismatches: flag cached objects whose path extension does not match the response Content-Type, for example an image or stylesheet path that returned HTML.
- Cacheable responses with identity material: scan origin responses for Set-Cookie, Authorization-dependent content or user identifiers combined with
publicor a positives-maxage. - Unusual request inputs: monitor for unexpected forwarding headers, rarely used query parameters and odd path delimiters arriving at the edge, especially from a single source in short bursts.
- Configuration drift: track every change to cache rules, cache policies and edge TTL overrides through version control and review, as part of configuration monitoring.
- CI cache provenance: record which workflow and ref created each cache entry used by a release job.
Metrics
- Number of cache hits on paths classified as authenticated (target: zero).
- Percentage of authenticated responses carrying
Cache-Control: no-storeorprivate(target: 100%). - Number of cache rules that override origin
Cache-Control, each with a documented owner and justification. - Percentage of origin routes returning 404 rather than a dynamic page for unknown sub-paths.
- Percentage of release workflows that build without restoring shared caches.
Prevention and hardening
Cache key design
- Key what varies: if a header, cookie or parameter changes the response, include it in the cache key or send
Varyfor it. RFC 9111 Section 4.1 requires caches to revalidate rather than reuse a stored response when the request headers named byVarydo not match. - Otherwise, stop using it: better still, make the origin ignore inputs that should not affect a cacheable response. Build absolute URLs from configuration, not from request headers, and only trust forwarding headers set by your own edge.
- Normalise consistently: apply the same path normalisation (case, encoded characters, duplicate slashes, delimiters, trailing segments) at the cache and the origin, or normalise once at the edge and forward the normalised form.
- Keep cache rules narrow: cache static assets from dedicated paths or hostnames, never by extension across the whole site.
Safe caching rules for responses
| Response type | Header policy | Basis |
|---|---|---|
| Authenticated or personalised page or API response | Cache-Control: no-store (or private where browser caching is acceptable) | RFC 9111 Sections 5.2.2.5 and 5.2.2.7; ASVS 5.0 V14.3.2; CWE-525 |
| Response to a request carrying Authorization | Not reused by shared caches unless the response explicitly permits it; never add public to such responses | RFC 9111 Section 3.5 |
| Public static asset | public with max-age and, for shared caches, s-maxage; versioned file names | RFC 9111 Section 5.2.2.10 |
| Response that sets a cookie | Do not cache in shared caches; leave CDN and proxy defaults that skip Set-Cookie responses enabled | Cloudflare and NGINX documented defaults |
| Unknown or non-existent path | Return 404 or a redirect, not the nearest dynamic page | ASVS 5.0 V14.2.5 |
| Cached object generally | Only cache responses whose content type is expected for the resource | ASVS 5.0 V14.2.5; ASVS V14.2.2 for server-side caches |
Edge features
- Content type checks: Cloudflare Cache Deception Armor, enabled per Cache Rule under the Cache Key setting, refuses to cache a response whose extension does not match its Content-Type. Its documentation notes that origin
Cache-Controlor an Edge Cache TTL rule can override the protection, so it complements correct headers rather than replacing them. - Separate key from forwarding: on CloudFront, review every value an origin request policy forwards and confirm the origin does not use it to build cacheable responses.
- Purge capability: make sure the team can purge by URL and by tag quickly; it is the rollback for a poisoning incident.
Phased rollout
- Inventory: list every cache layer, its key definition, its rules and which team owns it.
- Classify routes: mark each route as public static, public dynamic or authenticated.
- Fix origin headers first: add
no-storeorprivateto all authenticated routes and 404 handling for unknown sub-paths. This is low risk and can ship in normal releases. - Audit unkeyed inputs: for each cacheable route, list request inputs the origin reads and confirm each is keyed or ignored.
- Tighten edge rules in stages: narrow cache rules and enable content type checks on one hostname, compare hit ratio and error rates, then extend. Roll back by restoring the previous rule version and purging.
- Test continuously: include cache poisoning and deception checks in authorised testing and in review of every edge rule change. See the Web Application Security Testing Guide.
Decision guidance
- Small sites: keep CDN defaults (no HTML caching, skip Set-Cookie) and set correct headers at the origin; avoid "cache everything" rules.
- High-traffic sites caching HTML: cache only anonymous variants, bypass cache whenever a session cookie is present, and key on every input that changes output.
- Header-led versus rule-led caching: letting origin headers drive caching keeps the decision with the code that knows whether a response is personal. Edge overrides are faster to change but drift from application reality, so restrict them to static paths.
Lessons from incidents
The Ultralytics PyPI compromise in December 2024 is the clearest public case of cache poisoning reaching a release. PyPI's analysis found the attack targeted the GitHub Actions cache used during the build, and malicious versions 8.3.41, 8.3.42, 8.3.45 and 8.3.46 were removed from PyPI. Its recommendations included avoiding shared caches during build and release, using Trusted Publishing and auditing risky workflow triggers. GitHub's own documentation warns that anyone who can open a pull request can read caches on the base branch and that cache contents are not signed or verified. The lesson carries over from web caches: anything that a lower-trust actor can write and a higher-trust process later reads without verification is a poisoning channel. See the GitHub and GitHub Actions Security Guide and the CI/CD Security Guide.
Framework mapping
| Framework | Reference | Relevance |
|---|---|---|
| MITRE CAPEC | CAPEC-141 Cache Poisoning | Attack pattern |
| MITRE CWE | CWE-524, CWE-525, CWE-444 | Sensitive data in caches; browser cache policy; smuggling as a poisoning route |
| MITRE ATT&CK | T1190 Exploit Public-Facing Application | Exploitation of internet-facing web services |
| OWASP ASVS 5.0 | V14.2.2, V14.2.5, V14.3.2 | Server-side caching of sensitive data, content type checks, anti-caching headers |
| NIST SP 800-53 Rev. 5 | SC-4, SI-10, CM-6, SR-11 | Information in shared resources, input validation, configuration settings, component authenticity for build artefacts |
| NIST CSF 2.0 | PR.DS-01, PR.DS-02, PR.PS-01 | Data at rest and in transit, configuration management |
Practitioner checklist
- Document the cache key and rules for every cache layer and assign an owner.
- Set
Cache-Control: no-storeorprivateon every authenticated or personalised response. - Return 404 for unknown sub-paths instead of falling back to a dynamic page.
- Cache static content only from dedicated paths or hostnames, never by extension site-wide.
- Key or ignore every request input that can change a cacheable response; never trust client-supplied forwarding headers.
- Align path normalisation between cache and origin.
- Enable content type versus extension checks where the CDN offers them.
- Alert on cache hits for authenticated paths and on extension and Content-Type mismatches.
- Build and publish releases without restoring shared CI caches written by lower-trust jobs.
Standards and references
- IETF: RFC 9111 HTTP Caching
- MITRE CAPEC: Cache Poisoning
- MITRE CWE: CWE-524 Use of Cache Containing Sensitive Information
- MITRE CWE: CWE-525 Use of Web Browser Cache Containing Sensitive Information
- OWASP: Application Security Verification Standard 5.0
- Cloudflare: Cache Deception Armor
- AWS: Controlling origin requests with CloudFront
- PyPI Blog: Ultralytics attack analysis
Related NHI Mgmt Group resources: Web Application Attacks Guide · Security Headers Guide · Software Supply Chain Security Guide · CI/CD Pipeline Identity Security Guide