Skip to content

Cache key & Vary

Probes what actually varies the cache key, rather than trusting the declared Vary header alone.

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.

FindingSeverityMeaning
Undeclared cache-key input: <dimension>mediumVarying 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>infoDeclared in Vary, but varying it produced no observable difference — likely inflates cache fragmentation for no benefit

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.

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:

ObservationFindingSeverity
The isolated request is refused or differsAuthenticated response served to unauthenticated request — the origin withholds the content and a cache replayed it anywaycritical
The isolated request is also a cache HIT on the same bodySame finding, but the cache ignores the query string so the origin couldn’t be observed in isolation — confirm the content is meant to be publichigh
The isolated request matches and isn’t a HITNone — 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.