The one test that proves no user can see another user's data
Every multi-tenant app has the same nightmare bug: a route that forgets to check who owns the thing it returns. One missing where owner_id = ? and user B can read user A's projects by changing an id in the URL. Code review doesn't reliably catch it. Neither do the tests most of us write, because we test the routes we remember, and the leaky one is always the route we forgot. So I stopped writing…
Every multi-tenant application has a nightmare problem: a route that fails to verify who owns the data it returns. A single oversight, such as missing a check for owner_id = ?, allows user B to access user A's projects by simply altering an ID in the URL. Traditional code reviews and commonly written tests often fail to catch this issue, as developers tend to focus on the routes they remember and overlook the ones they accidentally leave vulnerable.
To address this, I stopped relying on per-route authorization tests and created a single comprehensive test that examines every route in the API.
First, collect all the routes the server registers automatically. Then, create two user accounts: Alice and Mallory. Alice will create real data, including a project, a file, a secret, and an API key. For every route that accepts an ID, call it using Mallory's account while using Alice's IDs. Every single route must return a 404 error, indicating that the data does not exist for Mallory.
Additionally, every route must return a 401 error when no session is present. The only exceptions are routes that display data an owner has chosen to publish, and these should be documented with their reasons.
In Fastify, register a hook before your routes to collect the list of routes. Append each route's method and URL to a routeList array. Decorate the Fastify app with routeList. For Express users, you can access app._router.stack or use an equivalent method depending on the framework you are using. Define a SHARED object that maps shared routes, such as GET /showcase/:id (publicly shared) and GET /shared-chats/:slug (a chat published with a link).
Next, create a function called fill that takes a URL and replaces placeholders with Alice's real IDs, the generation ID, and the ALICE_KEY. Iterate through all routes with IDs using the withIds variable. For each route, generate a URL by calling fill with the route's URL. Invoke app.inject with the appropriate method, URL, Mallory's cookies, and plausibleBody for the route method.
If the response status code is not 404, record the leak. If the response body includes "alice only," it means Alice's data was displayed, which should be recorded as a leak.
Also, invoke app.inject without cookies but include plausibleBody for the route method. If the response status code is not 401, it indicates a leak. Finally, expect the leaks array to be empty, ensuring that no route has been missed or improperly authorized.
Written by urgent.news from Dev.to's reporting — not their text. Machine-written — may contain errors; check the original before relying on it.