Testing web cache deception: when a fake file extension caches a private page
A shared cache and an origin server can disagree about what a URL is. The origin routes by matching a
pattern and often ignores a trailing segment, so a request for an account page with a made-up static suffix
still returns the account page, authenticated and personal. The cache, meanwhile, decides what to store by a
simpler rule, often the apparent file extension or path prefix, and treats that same URL as a harmless
static asset it may cache for everyone. When both are true at once, an attacker lures a logged-in victim to
the crafted URL, the origin serves the victim's private response, and the cache stores it under a key the
attacker can then request unauthenticated, retrieving the victim's data. This is distinct from cache
poisoning: nothing in the response is altered; a private response is simply cached where it should never be.
The bug is a cacheability decision that diverges from the origin's notion of the same, authenticated
resource. You find it by comparing what the cache stores against what the origin actually served.
When to use
- A shared cache or content delivery layer sits in front of authenticated, per-user dynamic responses.
- The cache decides cacheability by file extension, path prefix, or a static-looking pattern.
- The origin ignores or tolerates extra path segments, suffixes, or delimiters when routing a request.
Scope check
Test cache deception only against systems you own or are authorized to assess, on non-production
infrastructure, using two test accounts you control so any cached private response is your own, never a real
user's data. A confirmed case exposes an authenticated response to an unauthenticated request, so keep every
probe within the authorized scope and prefer an isolated instance. If you can't name the authorization, stop.
The loop
Establish whether the origin serves authenticated content for the crafted path and the cache stores it,
both at once, first. Confirm two things together: that the origin returns the same private, authenticated
response for the static-looking URL as for the real one, and that the shared cache actually stores that
response. This is the false-positive killer: if the origin returns a not-found or a login redirect for the
crafted path, or the cache does not store the response, there is no deception, and a URL that merely looks
static is not a finding. Name both behaviors before claiming impact.
Map the cache's cacheability rule. Determine what makes the cache store a response: a list of
extensions, a static path prefix, the absence of certain headers, or a pattern. This is the rule an
attacker will try to satisfy with a URL the origin still treats as the authenticated resource.
Craft the static-looking variant. Depending on the rule, append a cacheable-looking extension after the
real path, add a path segment or a delimiter the origin ignores but the cache reads as a different or
static resource, or use an encoded separator that the two parse differently. The goal is one URL the cache
deems cacheable and the origin routes to the private response.
Verify the origin still returns the private response. Request the crafted URL as an authenticated user
and confirm the response is the personal, authenticated content, not an error or a static file. If the
origin instead returns a generic asset or refuses, the deception does not hold for that variant.
Verify the cache stores and serves it to a different requester. Request the crafted URL again without
the victim's session, from a second account or unauthenticated, and confirm the cache returns the first
user's private response rather than re-authenticating. Cache-status headers, timing, and the returned
personal content distinguish a stored hit from a fresh origin fetch.
Confirm and record. Confirm end to end on an isolated instance with two test accounts: user one loads
the crafted URL, user two (or an unauthenticated request) retrieves user one's private response from the
cache. Kill the lead if the origin does not serve authenticated content for the crafted path, if the cache
does not store the response, if the cache re-validates authorization, or if the cache key already
distinguishes the authenticated resource. Record the crafted URL, the cache rule, the origin behavior, and
the retrieved private content, or set a kill_reason.
Where cache deception leaks
- The cache and origin disagree about the resource. The whole bug is a cacheability rule that says static
while the origin serves a private page for the same URL; find the gap between the two notions of the URL.
- Extension rules are the classic lever. A cache that stores anything ending in a known static extension
will store an account page when the origin ignores an appended suffix.
- Delimiters and extra segments confuse routing. A path delimiter or an extra segment the origin tolerates
but the cache reads differently produces one URL with two meanings.
- It is theft, not alteration. Unlike poisoning, the response is unchanged; the harm is that a private
response is stored under a key an attacker can fetch, so the finding is where it is cached, not what it says.
- The victim only has to visit. The attacker supplies the crafted URL; the victim's own authenticated
request populates the cache, after which the attacker reads it, so a single visit is the trigger.
Worked example (a confirm and a kill)
Confirm. An account page is served by the origin for its path with an appended made-up static
extension, returning the same authenticated content, while the shared cache stores any response whose URL
ends in that extension. User one visits the crafted URL and the cache stores their personal account page;
an unauthenticated request for the same URL returns user one's data from the cache on an isolated instance.
Confirmed web cache deception exposing authenticated content, high, remediation = cache by the
origin's content type and explicit cache-control rather than the apparent extension, do not cache responses
to authenticated requests, and make the origin reject or normalize unexpected suffixes and delimiters
instead of ignoring them.
Kill. The origin returns a not-found for the appended-extension URL rather than the account page, the
cache honors the origin's no-store on authenticated responses and keys on the normalized path, and a
repeat request without the session re-authenticates at the origin. The crafted URL yields no stored private
response. Killed, kill_reason = "the origin does not serve authenticated content for the crafted path
and the cache does not store authenticated responses; no private response is ever cached for an attacker to
retrieve."
Rationalizations to reject
- "That URL is not a real page." -> If the origin ignores the suffix and returns the real authenticated
page anyway, the URL is real enough; test what the origin serves, not what the route table intends.
- "The cache only stores static files." -> It stores what its rule calls static, which a crafted extension
or delimiter can satisfy while the origin serves a private page; the rule, not the reality, decides.
- "Authenticated responses are not cached." -> Confirm that for the crafted URL specifically; the deception
works precisely when the cacheability rule fires before the authenticated nature is considered.
- "This is just cache poisoning." -> It is the reverse: nothing in the response is altered, a private
response is stored where an attacker can read it; the analysis and fix differ.
- "The cache key includes the session." -> Then confirm it does for this URL shape; deception thrives where
the key is the apparent-static path and omits the session for URLs the rule deems cacheable.
Executing this in practice
You need the cache's cacheability rule, the origin's routing tolerance for suffixes, extra segments, and
delimiters, and the cache key it computes for each URL shape. For each authenticated resource, craft a
static-looking variant, confirm the origin still returns the private response, and confirm a second requester
retrieves it from the cache. Two test accounts on an isolated instance make the theft observable end to end;
cache-status headers and the returned personal content confirm a stored hit rather than a fresh fetch.
Related
testing-web-cache-attacks - the poisoning side of shared-cache abuse, where the response is altered rather
than stolen; the two share the cache-key and cacheability analysis from opposite directions.
hunting-host-header-and-url-parsing-trust - the origin-versus-cache disagreement about a URL is a
parser-differential problem that skill treats generally, including cache keying.
hunting-broken-object-level-authorization - cache deception discloses another user's authenticated
response, reaching the same private data that skill pursues through direct authorization flaws.
auditing-error-handling-and-information-exposure - a cached authenticated response is sensitive
information reaching an unauthorized requester, the exposure that skill audits by another route.
- FINDING-SCHEMA.md - source = the crafted static-looking URL, sink = the shared
cache storing the authenticated response, evidence = a second requester retrieving the first user's private
response from the cache on an isolated instance.
1---2name: testing-web-cache-deception3description: Test web cache deception, where an attacker crafts a static-looking URL, using an added extension, a path delimiter, or a query variant, that maps to the same authenticated dynamic response while a shared cache, keying on the apparent extension or path, stores that private response and serves it to the attacker. Use when a shared cache sits in front of authenticated content and its rule for what is cacheable can disagree with what the origin treats as the same resource. Covers extension-based caching rules, path and delimiter confusion, and cache-key versus origin-routing mismatches. The crafted static-looking URL is the source, the shared cache that stores the authenticated response is the sink, and the victim's private response being cached and retrieved by the attacker is the bug.4license: MIT5---67# Testing web cache deception: when a fake file extension caches a private page89A shared cache and an origin server can disagree about what a URL is. The origin routes by matching a10pattern and often ignores a trailing segment, so a request for an account page with a made-up static suffix11still returns the account page, authenticated and personal. The cache, meanwhile, decides what to store by a12simpler rule, often the apparent file extension or path prefix, and treats that same URL as a harmless13static asset it may cache for everyone. When both are true at once, an attacker lures a logged-in victim to14the crafted URL, the origin serves the victim's private response, and the cache stores it under a key the15attacker can then request unauthenticated, retrieving the victim's data. This is distinct from cache16poisoning: nothing in the response is altered; a private response is simply cached where it should never be.17The bug is a cacheability decision that diverges from the origin's notion of the same, authenticated18resource. You find it by comparing what the cache stores against what the origin actually served.1920## When to use2122- A shared cache or content delivery layer sits in front of authenticated, per-user dynamic responses.23- The cache decides cacheability by file extension, path prefix, or a static-looking pattern.24- The origin ignores or tolerates extra path segments, suffixes, or delimiters when routing a request.2526## Scope check2728Test cache deception only against systems you own or are authorized to assess, on non-production29infrastructure, using two test accounts you control so any cached private response is your own, never a real30user's data. A confirmed case exposes an authenticated response to an unauthenticated request, so keep every31probe within the authorized scope and prefer an isolated instance. If you can't name the authorization, stop.3233## The loop34351. **Establish whether the origin serves authenticated content for the crafted path and the cache stores it,36 both at once, first.** Confirm two things together: that the origin returns the same private, authenticated37 response for the static-looking URL as for the real one, and that the shared cache actually stores that38 response. This is the false-positive killer: if the origin returns a not-found or a login redirect for the39 crafted path, or the cache does not store the response, there is no deception, and a URL that merely looks40 static is not a finding. Name both behaviors before claiming impact.41422. **Map the cache's cacheability rule.** Determine what makes the cache store a response: a list of43 extensions, a static path prefix, the absence of certain headers, or a pattern. This is the rule an44 attacker will try to satisfy with a URL the origin still treats as the authenticated resource.45463. **Craft the static-looking variant.** Depending on the rule, append a cacheable-looking extension after the47 real path, add a path segment or a delimiter the origin ignores but the cache reads as a different or48 static resource, or use an encoded separator that the two parse differently. The goal is one URL the cache49 deems cacheable and the origin routes to the private response.50514. **Verify the origin still returns the private response.** Request the crafted URL as an authenticated user52 and confirm the response is the personal, authenticated content, not an error or a static file. If the53 origin instead returns a generic asset or refuses, the deception does not hold for that variant.54555. **Verify the cache stores and serves it to a different requester.** Request the crafted URL again without56 the victim's session, from a second account or unauthenticated, and confirm the cache returns the first57 user's private response rather than re-authenticating. Cache-status headers, timing, and the returned58 personal content distinguish a stored hit from a fresh origin fetch.59606. **Confirm and record.** Confirm end to end on an isolated instance with two test accounts: user one loads61 the crafted URL, user two (or an unauthenticated request) retrieves user one's private response from the62 cache. Kill the lead if the origin does not serve authenticated content for the crafted path, if the cache63 does not store the response, if the cache re-validates authorization, or if the cache key already64 distinguishes the authenticated resource. Record the crafted URL, the cache rule, the origin behavior, and65 the retrieved private content, or set a `kill_reason`.6667## Where cache deception leaks6869- **The cache and origin disagree about the resource.** The whole bug is a cacheability rule that says static70 while the origin serves a private page for the same URL; find the gap between the two notions of the URL.71- **Extension rules are the classic lever.** A cache that stores anything ending in a known static extension72 will store an account page when the origin ignores an appended suffix.73- **Delimiters and extra segments confuse routing.** A path delimiter or an extra segment the origin tolerates74 but the cache reads differently produces one URL with two meanings.75- **It is theft, not alteration.** Unlike poisoning, the response is unchanged; the harm is that a private76 response is stored under a key an attacker can fetch, so the finding is where it is cached, not what it says.77- **The victim only has to visit.** The attacker supplies the crafted URL; the victim's own authenticated78 request populates the cache, after which the attacker reads it, so a single visit is the trigger.7980## Worked example (a confirm and a kill)8182> **Confirm.** An account page is served by the origin for its path with an appended made-up static83> extension, returning the same authenticated content, while the shared cache stores any response whose URL84> ends in that extension. User one visits the crafted URL and the cache stores their personal account page;85> an unauthenticated request for the same URL returns user one's data from the cache on an isolated instance.86> **Confirmed** web cache deception exposing authenticated content, `high`, remediation = cache by the87> origin's content type and explicit cache-control rather than the apparent extension, do not cache responses88> to authenticated requests, and make the origin reject or normalize unexpected suffixes and delimiters89> instead of ignoring them.90>91> **Kill.** The origin returns a not-found for the appended-extension URL rather than the account page, the92> cache honors the origin's no-store on authenticated responses and keys on the normalized path, and a93> repeat request without the session re-authenticates at the origin. The crafted URL yields no stored private94> response. **Killed**, `kill_reason` = "the origin does not serve authenticated content for the crafted path95> and the cache does not store authenticated responses; no private response is ever cached for an attacker to96> retrieve."9798## Rationalizations to reject99100- *"That URL is not a real page."* -> If the origin ignores the suffix and returns the real authenticated101 page anyway, the URL is real enough; test what the origin serves, not what the route table intends.102- *"The cache only stores static files."* -> It stores what its rule calls static, which a crafted extension103 or delimiter can satisfy while the origin serves a private page; the rule, not the reality, decides.104- *"Authenticated responses are not cached."* -> Confirm that for the crafted URL specifically; the deception105 works precisely when the cacheability rule fires before the authenticated nature is considered.106- *"This is just cache poisoning."* -> It is the reverse: nothing in the response is altered, a private107 response is stored where an attacker can read it; the analysis and fix differ.108- *"The cache key includes the session."* -> Then confirm it does for this URL shape; deception thrives where109 the key is the apparent-static path and omits the session for URLs the rule deems cacheable.110111## Executing this in practice112113You need the cache's cacheability rule, the origin's routing tolerance for suffixes, extra segments, and114delimiters, and the cache key it computes for each URL shape. For each authenticated resource, craft a115static-looking variant, confirm the origin still returns the private response, and confirm a second requester116retrieves it from the cache. Two test accounts on an isolated instance make the theft observable end to end;117cache-status headers and the returned personal content confirm a stored hit rather than a fresh fetch.118119## Related120121- `testing-web-cache-attacks` - the poisoning side of shared-cache abuse, where the response is altered rather122 than stolen; the two share the cache-key and cacheability analysis from opposite directions.123- `hunting-host-header-and-url-parsing-trust` - the origin-versus-cache disagreement about a URL is a124 parser-differential problem that skill treats generally, including cache keying.125- `hunting-broken-object-level-authorization` - cache deception discloses another user's authenticated126 response, reaching the same private data that skill pursues through direct authorization flaws.127- `auditing-error-handling-and-information-exposure` - a cached authenticated response is sensitive128 information reaching an unauthorized requester, the exposure that skill audits by another route.129- [FINDING-SCHEMA.md](../../FINDING-SCHEMA.md) - source = the crafted static-looking URL, sink = the shared130 cache storing the authenticated response, evidence = a second requester retrieving the first user's private131 response from the cache on an isolated instance.