Docs / Finding bugs / Cache analysis
Cache analysis
The short version. Caching bugs — web cache deception and cache poisoning — leak one user's private response to another, or let an attacker plant a poisoned response for everyone. Crusader's Cache screen reads the traffic you've already captured and flags the routes where that can happen: shared-cache HITs, public/no-Vary responses sent with auth, identity crossings, static-looking paths returning dynamic private content, and reflected routing headers. It is a read-only triage lens — it sends zero probes and surfaces indicators, not confirmed bugs. You confirm the real ones by hand in Repeater on a target you're authorized to test.
01What the Cache lens is (and isn't)
The Cache page (sidebar, under PAGES) is a passive analyzer that runs over your project's captured History. It groups requests by endpoint, reads the cache-relevant headers on each response, and scores the result for cache-related risk. Nothing on this screen touches the network: the topbar pills read observed only, 0 probes, and nothing sent, and the stats band says all data observed / 0 probes sent. That is the literal behavior — the screen only ever looks at bytes the proxy already saw.
Because it's observational, it's part of Crusader's Free tier — a real daily driver, not a teaser. The trade-off is honest: this is an indicators-and-triage tool. It cannot prove a caching bug on its own, because proving one means re-issuing a request (as a second user, from a clean session, or with an attacker-controlled header) and watching what the cache does. Crusader leaves that step to you — every finding ships with a SAFE CONFIRMATION checklist and a 0 requests sent label so there's no ambiguity about what's been verified.
The Cache page only considers cacheable methods — GET and HEAD — and only entries with real cache evidence. Static assets, JSON bundles, and tracker noise are hidden by default; toggle JSON/STATIC HIDDEN if you need to see them.
02What it surfaces
For every endpoint with cache evidence, Crusader parses the response headers and derives three things: whether the response is cacheable, whether it was actually served from a shared cache, and whether that's a problem given what the request carried. The raw signals it reads:
| Signal | Read from |
|---|---|
| Cacheability | Cache-Control (public / private / no-store / no-cache / max-age / s-maxage / stale-*), CDN-Cache-Control, Surrogate-Control, Expires, Pragma |
| Shared-cache HIT | CF-Cache-Status, Cache-Status, X-Cache, X-Cache-Hits, X-Served-By, X-Varnish, plus Age > 0 |
| Cache layer | Cloudflare, Fastly, Varnish, CloudFront, or a generic shared cache, inferred from those headers + Via / CF-Ray |
| Freshness / TTL | max-age (or s-maxage) minus Age, shown as a live countdown and an Age-reset sparkline |
| Key inputs | Vary tokens vs. the credential the request actually carried (Cookie / Authorization / API-key) |
| Auth context | request Cookie (real session vs. analytics-only), Authorization, X-Api-Key; matched against your saved Identities when possible |
From those it raises scored risk classes. The ones worth knowing:
- Public + auth'd — a sensitive response went out
Cache-Control: publicwhile the request carried a real credential. Classic shared-cache leak setup. - No Vary on auth — a sensitive, shared-cacheable response whose
Varydoesn't coverCookie,Authorization, or the API-key header it was keyed by. - Identity crossing — the same response body signature was observed behind two different real credentials. The strongest pre-confirmation signal that one user can receive another's cached object.
- Cache deception candidate — a static-looking path (or a
;/%2e/..trick) returned dynamic private HTML or JSON. - ACAO unkeyed —
Access-Control-Allow-Originreflects the requestOriginon a cacheable response that doesn'tVary: Origin. - Header-poison candidate — a routing header (
X-Forwarded-Host,X-Forwarded-Proto,X-Forwarded-Prefix,X-Host,X-Original-URL,Forwarded, …) was present on a cacheable response and isn't inVary— or its value was reflected intoLocation/ CORS. - Plus: Set-Cookie on a shared-cacheable response, GraphQL cache risk, Next.js/RSC variant, partial-fragment (HTMX/Turbo/Inertia), OAuth/SAML cache, cached redirect poison, ETag cross-identity, HEAD/GET confusion, and route-family signature collision.
Crusader is deliberately conservative. A CDN layer alone, a browser-cacheable max-age, or a CF-Cache-Status: DYNAMIC/MISS/BYPASS is not treated as a shared-cache risk — findings are capped low unless real HIT/Age/shared-storage evidence survives the private / no-store / no-cache / Set-Cookie filter. Fewer, truer flags beat a wall of CDN noise.
03Open it and read the board
Capture some authenticated traffic first — browse the target through Crusader's proxy while logged in, ideally as more than one account, so the lens has identities to compare. Then open Cache from the sidebar.
The header reads N cacheable URLs, observed. with stat tiles for CACHEABLE URLS, LAYERS, AGE RESETS / HR, IDENTITY CROSS, WORTH A LOOK, and ASSETS HIDDEN. Below that, filter chips narrow the list:
| Filter | Shows |
|---|---|
| Findings | Entries the scoring engine elevated into a scored cache finding. |
| Cached | Entries with observed shared-cache HIT / Age evidence. |
| Sent with auth | Entries where the request carried a real credential. |
| Public + auth'd | The high-value public-cacheable-while-authenticated set. |
| No Vary on auth | Shared-cacheable auth responses missing credential-aware Vary. |
| Identity crossing | Same response seen behind two different identities. |
| Worth a look | Everything above the triage threshold, in priority order. |
You can also group the list by Layer, TTL, Host, or None, and search by URL, host, or header. Start on Findings or Identity crossing — that's where the reportable bugs cluster.
04Triage a finding
Click any entry to open the detail panel on the right. It's built for exactly one decision: is this worth a manual replay? The sections, top to bottom:
- Cache finding — severity (Critical / High / Medium / Low / Info), a 0–100 score, the evidence strings, and a SAFE CONFIRMATION list of read-only steps to prove it.
- Proof lane — the best observed auth → anonymous pair for this URL, with whether the two responses share a signature, fell inside the TTL window, and showed a cache HIT. This is the report-grade path.
- Header evidence — the raw cache and auth headers pulled from History (request + response), so you're reading ground truth, not a paraphrase.
- Cache topology — a You → CDN → Origin chain with the hop that's hot, plus the trust-boundary note (e.g. "public was observed on a request that carried authentication").
- Cache key (inferred) — two columns: inputs that changed the response ("appears keyed") vs. inputs that varied while the response stayed identical ("same public response across variants"). The second column is your unkeyed-input shortlist. Labelled 0 requests sent — this is inferred from captures, not probed.
- Identity / Stranger / Timing — who hit the entry, the cross-session pairing, and the Age timeline.
The detail header carries the actions you'll actually use: Repeater (send the captured request straight to a tab to replay it), Proof pack (copy a full text report — URL, layer, status, identities, age resets, evidence, the auth/stranger pair, inferred key signals, and the raw cache headers), History, Popout, and Ignore.
Two different identities hitting the same URL is normal multi-user traffic, not a bug. Crusader only colors it as a crossing when the same private response signature appears behind different credentials. Read the score's evidence, not just the chip count.
05Web cache deception
Web cache deception is when a static-looking URL — /account/settings.css, /api/me;.js, a %2e-encoded path — is treated as cacheable by the CDN but served dynamic, private content by the origin. The cache then stores one user's private page under a URL anyone can request.
Crusader flags these as cache deception candidates: it spots a static extension or path trick (;, %2e, %2f, ..) on a response that was actually HTML- or structured-content, carried a real credential, and shows shared-cache risk evidence. Crusader's scanner has a matching passive module (web-cache-passive) that goes further on the body: when a static-extension path returns dynamic content, it scans for per-user markers — emails, names, user/tenant IDs, JWTs, session cookies, API keys — and rates confidence by whether the response was an observed cache HIT and whether the leaked value is credential-like.
The confirm step is always the same shape: in Repeater, append a harmless static suffix variant to the private URL and check whether the private HTML/JSON becomes cacheable and is then served from cache to a clean client.
06Cache poisoning candidates
Cache poisoning is the inverse: an attacker sends a request with an unkeyed input — a routing header the cache ignores when building its key — that changes the cached response everyone else then receives. The classic vector is X-Forwarded-Host rewriting an absolute URL in the page, but redirects, CORS headers, CSP, and script sources are all in play.
Crusader surfaces this as a header-poison candidate when a cacheable response (a) carried one of the routing headers it watches and that header isn't in Vary, or (b) reflected that header's value into Location or Access-Control-Allow-Origin. The scanner module reports the same class — it extracts the reflected value, checks it actually appears in the body (raw, URL-encoded, or HTML-encoded), and labels the result explicitly:
# scanner verdict for an unkeyed-header poisoning candidate
X-Forwarded-Host value `evil.example` appears in a cacheable
response, and Vary does not include X-Forwarded-Host. This is a
passive-only indicator: replay with an attacker-controlled value
to confirm whether the shared cache absorbs it.
There is no active cache-poisoning engine. Crusader will not send a poisoned request, will not plant a payload, and will not auto-verify the bug — by design. It points you at the surface; you prove it.
Replaying with a forged routing header is intrusive — you may poison a real shared cache entry for real users. Only do it against a target you're explicitly authorized to test, use a unique attacker-controlled value you can recognize, and clean up. The web-cache-passive module is also part of the passive scanner; its validation step returns "needs manual review", never an automatic proof.
07Confirm it for real
Indicators become findings when you reproduce them. The repeatable loop, all of it Free:
- On the finding, read the SAFE CONFIRMATION steps and copy the Proof pack for your notes.
- Click Repeater to load the captured request into a tab.
- Prime as one identity, replay as another. For a cross-user leak: send as Identity A, then set the tab's identity to B (or strip auth entirely) and send the same URL with no cache buster. Compare status, body hash,
ETag,Age, and any A-specific markers. For deception: add the static suffix and check whether private content becomes cacheable. For poisoning: send your unique routing-header value, then re-request as a clean client and see whether your value persists in the cached response. - If the cached object crosses the trust boundary, you have a real bug. Promote it into your report from the proof pack.
A few honesty notes to keep your reports clean: the inferred cache-key columns are derived from observed captures, not active probing, so treat them as leads to test rather than proof. Vary-based reasoning assumes the upstream honors it. And matching to your saved Identities sharpens the "real credential vs. analytics cookie" call — set those up on Target scope / Identities before a long session so crossings are scored accurately.
For chained or active follow-up — turning a cache-deception leak into an authorization finding, or running the broader passive scanner across the whole project — see the scanner guide. The passive scanner (including the web-cache module) is Free; active proof replay for the classes that support it is Hunter Pro, and authorized-targets-only.
Want a guide that isn't here yet? Email hello@crusaderproxy.com.