← Debugging Guides

阅读中文版本

HTTP Caching — Authenticated Responses Leak Through Shared Caches

Prevent private data leaking through CDN and intermediary caches, understand why authenticated responses need explicit Cache-Control private, and audit cache keys.

advanceddebuggingsecuritycachinghttp

Symptoms

  • A user sees another user data briefly, or receives cached personalized content after logout
  • The CDN serves personalized JSON with HIT status to different sessions
  • Reproduces only through the CDN domain; direct origin requests behave correctly

Possible causes

  • Authenticated responses lack Cache-Control: private or no-store, so shared caches treat them as cacheable
  • The CDN cache key ignores the Authorization header or session cookie variant
  • A logout response reuses an authenticated URL without invalidating the cached variant
  • An origin default (200 cacheable heuristic) applies because no Cache-Control header was sent at all

Diagnosis steps

Check what the origin actually sends

curl -sI https://origin.internal/api/me -H "Authorization: Bearer alice" | grep -iE "cache-control|vary|age"

Interpretation

No Cache-Control on an authenticated response means heuristic caching applies downstream — the leak starts at the origin header, not the CDN.

Inspect the CDN cache key

curl -sI https://app.example.com/api/me -H "Authorization: Bearer alice" | grep -iE "x-cache|age|cf-cache-status" # then with bob token

Interpretation

A HIT for the second user with the same URL proves the Authorization header is not part of the cache key or the variant selection.

Audit personalized endpoints

grep -Rn "res.json(user\|ctx.body = user" src/routes/ | head

Interpretation

Every handler returning per-user content is a candidate for the private header audit.

Root cause

HTTP caches store what responses permit them to store. Without an explicit Cache-Control for authenticated content, shared caches may apply heuristic caching and serve one user personalized response to another requesting the same URL — especially when the cache key omits Authorization or the Vary header does not include the credential dimension.

Fix

Send Cache-Control: private, no-store (or private with a short max-age where genuinely safe) on every authenticated response, include Authorization in the CDN cache key or bypass CDN caching for credentialed paths, and set Vary: Authorization where variants are legitimate.

Risk note

Cache-Control: private with a long max-age still leaks across accounts on shared machines via the browser cache. Use no-store for anything sensitive.

Verify the fix

Two different sessions requesting the personalized endpoint each receive their own data with MISS (or bypass) cache status; logout responses carry no-store and are never replayed.

Symptom walkthrough

The leak has a hard reproduction boundary: through the CDN domain, user B sometimes receives user A cached response; direct to origin, always correct. The headers explain it:

$ curl -sI https://app.example.com/api/me -H "Authorization: Bearer bob"
HTTP/2 200
content-type: application/json
x-cache: HIT            <- cached variant served to bob
age: 41

HIT for a personalized endpoint means the cache key does not include the credential. Ask the origin what it declared: if /api/me returns no Cache-Control at all, downstream caches may store it under heuristic rules — the bug begins in the missing header, and the CDN merely executes it.

Fix in practice

State the policy explicitly at the origin:

app.get('/api/me', auth, (req, res) => {
  res.set('Cache-Control', 'private, no-store'); // per-user: never shared
  res.json(req.user);
});

app.get('/api/catalog', (req, res) => {
  res.set('Cache-Control', 'public, max-age=300, stale-while-revalidate=60');
  res.json(catalog); // genuinely identical for everyone
});

At the CDN, keep the key honest for any credentialed path you do cache:

cache key: scheme + host + path + query + $http_authorization
# or simpler and safer: bypass cache when Authorization is present

And for authenticated variants that legitimately share a URL, Vary: Authorization makes the dimension explicit — but treat Vary on credentials with suspicion: intermediaries are historically sloppy about it, and no-store remains the safe default for sensitive payloads.