The agent cached a GET by path and handed one tenant another tenant's row
The agent was asked to speed up a slow invoice-listing endpoint. It added a memoization layer in front of the handler, keyed the entry on the request path, warmed the cache with one sample call locally, watched the secon
The agent was asked to speed up a slow invoice-listing endpoint. It added a memoization layer in front of the handler, keyed the entry on the request path, warmed the cache with one sample call locally, watched the second call return instantly, and committed. Unit tests stayed green because every fixture ran as the same fixed user, so the cache never had to distinguish who was asking. In staging, two test accounts differed, and the second account read the first account's row.
The agent's assumption narrowed to the code it could read. It optimized the response it could see and treated the URL path as the identity of the request, when the path names the operation, not the caller. The authenticated subject lives in the token and the session, two inputs a memoization key built only from the route cannot reach.
A path key assumes every caller shares the same data
The route identifies the operation, not the requester. /invoices is the same path for tenant A and tenant B. When the cache key holds only that path, the first response written defines what everyone else reads, with no check that the callers match.
Local and unit tests usually run one fixed principal. The cache-key logic therefore never gets exercised across two identities in the suite. The leak stays invisible wherever the harness does not switch users between reads.
A wrong row is quieter than a failed call. The response still validates against its schema, types line up, and the status stays 200, so type checks and contract tests stay green. Only the data belongs to someone else, and a plain smoke test will not catch that.
Key the cache on identity and the full request contract
Put the authenticated subject in the key. Tenant id, account id, or user id belongs in the cache namespace from the start, not as an afterthought. Key on path plus query plus identity, never path alone.
Add an isolation test between two fixtures. Write tenant A's row, read it as A, then read the same path as B and assert B receives its own data or a miss. That cross-identity check fails exactly on a path-only key.
Default to per-principal sharing. Mark genuinely global data, such as public reference tables, explicitly; everything else namespaces to the caller.
Invalidate under the same namespace on write. The entry set for tenant A clears when A's row changes, and it must never be eligible for B's lookup in the first place.
Memoization makes a fast endpoint faster; it also makes a shared cache repeat the first caller's answer for every caller. Where has a cache key collapsed two identities into one entry?
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.