Trace the whole response path

Capture headers at the application, gateway, CDN, and browser. A safe header at the app can be changed by a proxy rule. Make two test accounts with clearly different balances and call the same URL in order A, B, A. Repeat with query parameters and cookies. Compare body ownership and cache status on each hop. A cache hit is not itself a flaw; a hit that returns another customer’s data is.

Test cases and proof

Use test accounts and test data
CaseExpected resultProof to keep
A primes cache; B requests same pathB sees only B dataBody and cache headers
Logout then revisit cached pageSensitive response is not sharedBrowser and proxy trace
Change query orderNo cache-key collisionResponse owner

Synthetic example

Customer A opens /api/account/summary through the CDN. Customer B requests the same path. Compare the account ID in both responses and inspect cache headers. Never assume an Authorization header alone prevents an upstream cache from storing the body.

Evidence to keep

Create two balances that are visibly different, such as test values 101 and 202. Send requests through the same domain customers use so the real cache path is involved. Record response headers and the cache-status header from each hop. The failing proof is the wrong account’s body, even if the response code is 200 and the app screen looks normal.

Find the cache that made the decision

A cache header from the app is not enough when a CDN rule rewrites it. Capture a request ID and cache status at each layer. If B receives A’s body, disable shared caching for that route at once, then find the key or header that caused the hit. For private balance data, no-store is simple but raises origin load. A browser private cache and a managed shared cache need different rules. Any permitted managed cache must partition by identity and permission. Test permission change and logout, not only two steady accounts.

Related reading

Why these checks matter

MDN’s Cache-Control reference defines private and no-store response directives. OWASP’s authorization guide says access checks must cover each request. A shared cache sits between those two rules: if it serves one customer’s saved body to another, the app never gets a chance to run its check. Test the final edge response.

Choose the directive by storage purpose

For a statement that must not be stored by an HTTP cache, send Cache-Control: no-store and verify the edge’s actual rules. Private prevents shared-cache storage but still permits a browser’s private cache. No-cache permits storage and requires validation before reuse. These directives have different meanings; choosing one needs a written rule for the data.

Use fake balances A=101 and B=202. Prime the identical path as A, then call it as B and signed out. Capture body owner, Cache-Control, Age, ETag, Vary, and the managed cache’s hit indicator. Test Authorization and cookie flows separately. Never rely on a header name alone when a CDN is configured to override origin behavior.

Test conditional responses and local copies

Send A’s ETag in B’s If-None-Match request. A 304 has no body, but the client may reuse a body it already holds. Validate that B cannot combine that response with A’s cached private body. After permission removal, run a fresh request and a conditional one through the same edge. Purge old stored entries when changing the route policy.

Browser Back, service-worker Cache Storage, local databases, and native offline stores are separate checks. No-store is an HTTP cache instruction; it is not a promise to erase every app-managed copy. Sign out A, sign in B, and inspect these stores and the rendered balance before a network response. Keep each storage layer in the finding so its owner can fix it. RFC 9111 defines HTTP cache rules; app storage needs its own policy.

Source