A page seeds its client cache from a server-side copy shared between users, and a visitor sees another account's data. What went wrong, and how is it prevented?
answer
- who the value belongs to
- a shared read, then a shared document
- identity must be in the key
- private, no-store on personal responses
- impersonal shell, personal part per request
basics
~20 sA personalised value was read from a copy not keyed by identity, then frozen into a document free to be reused. Keep identity-dependent reads out of shared copies, and never let a document carrying a personal seed be shared.
solid answer
~50 sTwo failures compound. First, the render read a per-user value from somewhere shared — a process-level or cross-request store keyed by URL rather than by identity — so whoever rendered first put *their* data into the result. Second, the render's output, including the serialised seed, was itself eligible for reuse, so a document containing one person's data was served to another. Prevention works at both points: an identity-dependent read must either be keyed by identity or marked as non-shareable, and any response whose seed contains personal data must be marked private and uncacheable by shared caches. The structural fix is stronger — keep the shared, seedable shell free of personal values and let the personal part be requested per user, so the two can never be mixed in one stored artifact. Review what the seed contains, too: it is readable by anyone holding the page.
go deeper
Take away the rule: data that belongs to one person must never be read from a place that hands the same answer to everyone, because that answer can end up in someone else's page.
Separate the two failures — an identity-blind read produces the wrong value, and a reusable response distributes it. Be able to say which mitigation addresses which.
Demonstrate the diagnosis: request the route as two accounts and once signed out, diff the payloads, and inspect the delivered document rather than reasoning from the source.
Argue for the shape that removes the class: an impersonal shareable shell plus a per-request personal region, so no stored artifact can hold a personal value and no reviewer has to catch it by hand.
This is the failure the client handoff is famous for, and it is worth being precise about, because the seed is rarely the *cause* — it is the mechanism that makes the cause visible and durable. ## How one person's value reaches another person's document 1. The route's server-side read asks for something personal — the signed-in user's profile, cart, balance, entitlements. 2. That read is served from a copy shared across requests, addressed by something that does not include identity (a URL, a path, a query name with no user in the key). 3. The render places the value into both the markup and the serialised seed. 4. The resulting document is stored or reused — by an in-process render cache, a shared store, a CDN, anything that treats the response as reusable — and handed to the next visitor. The visitor's browser then does exactly what it is supposed to: it writes the seed into the client cache as the truth for *this* session. Their UI is now confidently displaying someone else's data, and it will keep displaying it until something invalidates the entry. ## Two independent failures, both required - **The read was shared although the value was not.** Anything keyed without identity will hand the second caller the first caller's answer. This is the root cause, and it exists whether or not the page ever seeds a client cache. - **The rendered output was reusable.** A response carrying personalised data that is not marked private can be stored by a shared cache and replayed. On its own this leaks the *markup* too; the seed simply guarantees the client keeps the wrong value in a structure it will read again on navigation. Assuming the document is private is not a substitute for fixing the read: a shared read produces the wrong value before caching enters the picture at all, and two concurrent requests can be served from one in-flight render regardless of any header. ## Containment at the seeding boundary - **Key by identity or refuse to share.** Any read whose answer depends on who is asking must include the identity in its key, or must be declared non-shareable so it re-executes per request. - **Mark the response.** A document whose seed holds personal data needs response headers that keep shared caches out of it — `Cache-Control: private, no-store` is the blunt, correct instrument — and must never be eligible for prerendering or reuse. - **Split shared from personal.** The most robust shape is a shell that is entirely impersonal, prerenderable and safely seedable, with the personal region filled per request or fetched by the browser after load. If no stored artifact ever contains a personal value, no stored artifact can leak one. - **Audit the payload's contents.** A seed is serialised whole. Over-fetching an entity on the server — the full user record when the header needed a display name — puts the rest of that record into the document, readable by anyone who has the page, including the legitimate owner who was never meant to see those fields in this context. ## Verifying it rather than assuming it 1. **Two-identity test.** Request the same URL as two different accounts and diff the serialised payloads. Identical bytes for two identities on a personalised route is the bug, in one step. 2. **Sign-out replay.** Fetch the route with no credentials and inspect the payload. Anything personal in it was served without an identity check. 3. **Inspect what is actually shipped.** Read the payload in the delivered document rather than reasoning about the code: what the render *intended* to expose and what the seed *contains* diverge routinely. ## What belongs in a seed - **Seed what this user's first screen needs, projected down to the fields it renders.** A narrow projection is smaller, faster and cheaper to review. - **Keep out anything whose audience is narrower than the page.** Admin-only fields, other users' records, internal identifiers and tokens have no business in a document, even when the current UI never renders them. The interview answer that lands is the one that separates the layers: the shared read is the defect, the reusable response is the amplifier, and the seed is what turns a momentary rendering mistake into state the victim's browser then treats as its own.
- How would you prove a route is free of this bug rather than arguing it is?Request the route as two different accounts and diff the serialised payloads; identical bytes on a personalised route is the bug. Then request it with no credentials at all and inspect what comes back. Both checks read the delivered document, not the source, because what the render intended and what the seed contains routinely differ.
- Which page shape makes this class of leak structurally impossible?One where no stored or reusable artifact ever contains a personal value: an impersonal shell that is prerendered and seeded freely, with the personal region rendered per request or fetched by the browser after load. Sharing is then safe by construction rather than by everyone remembering to key by identity.
- Why is over-fetching on the server worse on a page that seeds than on one that does not?A seed is serialised whole, so every field the server read is shipped to the browser whether or not anything renders it. On a page without a handoff the extra fields die on the server. Project the read down to what the first screen displays.
saying these in an interview costs you the question
- Blames the client cache rather than the identity-blind server read.
- Assumes marking the document private is enough when the read itself is shared.
- Thinks a personalised value is safe because the UI hides those fields.
- Seeds the whole user record and trims it in the component.
- Believes a signed-out visitor cannot receive a personalised payload.