Cache key & Vary
Probes what actually varies the cache key, rather than trusting the declared Vary header alone.
How it works
Section titled “How it works”For each of Accept-Encoding, Accept-Language, User-Agent, and a custom header, the check sends a request that varies only that input and compares the response against a baseline (a different cdn-detected cache-status value, or a different response body counts as “keyed”).
Why User-Agent needs special handling
cache-detective’s own probe transport (harnessx/probe) deliberately overwrites the User-Agent header on every request for consistent identification. Varying User-Agent on purpose therefore bypasses that transport for just this one probe (checkbase.DoRaw instead of checkbase.Do) — every other probe in cache-detective goes through the standard transport.
Findings
Section titled “Findings”| Finding | Severity | Meaning |
|---|---|---|
Undeclared cache-key input: <dimension> | medium | Varying this input changed the response, but it’s not declared in Vary — a cache-poisoning surface if the value is attacker-controlled |
Declared Vary dimension has no observed effect: <dimension> | info | Declared in Vary, but varying it produced no observable difference — likely inflates cache fragmentation for no benefit |
Request budget
Section titled “Request budget”This check issues extra requests to the live target but is not gated behind --aggressive — it’s read-only from the target’s point of view (it never plants content a poisoned cache would later replay; that’s Cache poisoning & deception’s job). It still respects --max-aggressive-requests as a hard cap on probe count per resource.
Credentials in the cache key
Section titled “Credentials in the cache key”Varying Accept-Language or User-Agent shows whether the cache keys on them. Credentials can’t be probed that way: a cache that ignores Authorization or Cookie gives the same response for a different valid token, and for no token at all. So when the scan carries credentials (--bearer, --cookie, or an auth profile), the check repeats the request with Authorization and Cookie stripped. Other configured headers stay on, so a custom API-key header isn’t stripped and goes unreported rather than misreported.
If that credential-less request comes back identical to the authenticated one, either the content is public or a cache replayed it. A second credential-less request, against a cache-buster-isolated URL no cache entry can answer, tells them apart:
| Observation | Finding | Severity |
|---|---|---|
| The isolated request is refused or differs | Authenticated response served to unauthenticated request — the origin withholds the content and a cache replayed it anyway | critical |
The isolated request is also a cache HIT on the same body | Same finding, but the cache ignores the query string so the origin couldn’t be observed in isolation — confirm the content is meant to be public | high |
The isolated request matches and isn’t a HIT | None — the origin serves this content to anyone | — |
This catches the missing Vary: Cookie case (a personalized page cached under a shared key) and a cache keyed on the path alone that ignores Authorization. It costs two requests from the --max-aggressive-requests budget.