Skip to content

[P2] Edge cache key omits X-WorldMonitor-Key: an invalid key on a warm public URL is served the cached anonymous 200, not a 401 #5386

Description

@koala73

Found while wiring the previously-inert live cache/auth suite into CI (#5379). The origin is correct; the edge cache key is the issue. No private data exposure demonstrated — filing for a product decision on the posture, and because the shape is the #4497 hazard.

Reproduction (production, read-only GETs)

# cache-busted URL, invalid key
curl -sD- 'https://api.worldmonitor.app/api/bootstrap?keys=weatherAlerts&cb=RANDOM' \
  -H 'X-WorldMonitor-Key: wm_0000000000000000000000000000000000000000'
  -> HTTP 401 · cache-control: no-store · x-vercel-cache: MISS      # correct

# same URL once warm, same invalid key
curl -sD- 'https://api.worldmonitor.app/api/bootstrap?keys=weatherAlerts' \
  -H 'X-WorldMonitor-Key: wm_0000000000000000000000000000000000000000'
  -> HTTP 200 · cache-control: public, s-maxage=600 · x-vercel-cache: HIT · age: 39

Same result on www. and for every malformed variant tried (wm_notcanonical, wm_x, uppercase hex, and a non-wm_ string). The response carries vary: Origin only — X-WorldMonitor-Key is not part of the cache key, so once a public URL is warm, a request bearing an invalid key is served the cached anonymous response instead of the origin's 401.

Why this matters

api/bootstrap-auth.test.mjs:302"weather-only bootstrap with malformed wm_ header is rejected instead of anonymous bypass" — asserts 401 for exactly this input and passes in-process. Production does the opposite whenever the cache is warm. The unit test cannot see it because the divergence lives entirely in the edge layer.

Impact today is bounded, and I checked rather than assumed:

  • The cached body is the anonymous public payload — invalid credentials are silently downgraded to anonymous, not leaked anything private.
  • Genuinely gated keys still fail closed: ?keys=marketData with an invalid key returns 401 no-store on both hosts regardless of cache state.

The reason to care is the shape, not today's blast radius. This is the same cache-key blindness as #4497: if an authenticated 200 ever became cacheable on one of these URLs, the identical mechanism would serve private data publicly. Right now the only thing standing between us and that is the origin correctly setting no-store on every authenticated response.

Decision needed

  1. Accept it — an invalid key on a public endpoint yielding public data is arguably fine, and adding the auth header to the cache key would fragment the cache and hurt hit rate. If so, the unit test's premise should be corrected so it stops asserting a contract production does not honor.
  2. Add X-WorldMonitor-Key to the cache key / Vary — makes production match the unit test, at a cache-efficiency cost.
  3. Bypass cache when the header is present — invalid keys always reach the origin and 401; anonymous traffic keeps full cache benefit. Probably the best ratio, but needs a CDN rule.

Already covered

The live suite (now running on a 6h schedule via .github/workflows/live-api-cache-auth.yml) pins this behavior explicitly in "PINNED: an invalid key on a publicly-cached URL is served the cached anonymous 200, not a 401". That test:

  • asserts the served payload is structurally the anonymous envelope (data/missing, and data containing only the requested public key), so if an entitled payload ever lands in a public cache entry it goes red;
  • passes cleanly if the behavior is later fixed to 401 (it takes the fail-closed branch and asserts no-store), so fixing this will not require touching the test.

The fake-auth assertions in that suite are cache-busted so they test origin auth deterministically rather than whatever the CDN happens to be holding.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions