Someone Else's Account Page, From Cache — JavaScript Bug Hunt

Modelled on Steam, 25 December 2015. While Steam was under a denial-of-service attack, a caching configuration change made to absorb the traffic caused some…

  • Language: JavaScript
  • Layer: Backend
  • Difficulty: Medium
  • Concepts: Caching, Security, Auth
  • Modelled on: Steam · 2015
  • Visible tests: public pages are cached; each signed-in user sees their own account page
  • Reward: 50 XP for a complete fix

Briefing

Modelled on Steam, 25 December 2015. While Steam was under a denial-of-service attack, a caching configuration change made to absorb the traffic caused some pages for signed-in users to be cached and served to other users. Valve said about 34,000 users' account pages may have been shown to someone else, and that the pages exposed no full card numbers and allowed no account changes.

This reconstruction's edge cache stores every successful GET by path, with no regard to who asked.

Fix handle so personal responses are never stored or served from cache.

Bug report

BUG-EDGE-1225 · Priority: Critical · Reported by: support (users seeing other accounts)

cache.create(origin).handle(req) returns { status, body, fromCache }:

  • only GET requests are served from / stored in the cache, keyed by path, and only status 200 responses are stored
  • a request carrying a cookie or authorization header (req.headers.cookie / req.headers.authorization) bypasses the cache entirely: it is neither answered from the cache nor stored
  • a response with a set-cookie header, or a cache-control header containing "private" or "no-store" (any case), is never stored
  • everything else ("public, max-age=60", no cache-control at all) is stored

Observed: users opening /account saw another user's name, email and purchase history.

Logs

[edge] GET /account cookie=sess-alice MISS -> stored
[edge] GET /account cookie=sess-bob HIT (age 3s)

The code as shipped

src/edge/cache.js (editable)

exports.create = function (origin) {
  var store = {};
  return {
    handle: function (req) {
      var cacheable = req.method === "GET";
      if (cacheable && store[req.path]) {
        return { status: 200, body: store[req.path].body, fromCache: true };
      }
      var res = origin(req);
      if (cacheable && res.status === 200) store[req.path] = res;
      return { status: res.status, body: res.body, fromCache: false };
    }
  };
};

Read-only context: src/edge/HTTP.js.

Open the hunt to edit the files, run the visible tests and submit against the hidden ones. More JavaScript bug hunts.