ASVS 5.0 V8.4.1 requires multi-tenant applications to use cross-tenant controls so that a consumer's operations never affect tenants they have no permission to interact with. It is a Level 2 requirement. The practical meaning: every read and every write is scoped to the caller's tenant, and that scope comes from the server, not from the request.
Why it matters
In a shared system, one missing WHERE tenant_id = ? is a breach that crosses customer boundaries. These bugs are common because each query looks fine on its own, and tests usually run with a single tenant.
How to implement it
Derive the tenant once, from the verified session or token, and make the data layer apply it automatically.
-- PostgreSQL row level security as a backstop
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
ALTER TABLE invoices FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON invoices
USING (tenant_id = current_setting('app.tenant_id')::uuid)
WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid);
await db.transaction(async (tx) => {
await tx.query("SELECT set_config('app.tenant_id', $1, true)", [req.auth.tenantId]);
// all queries in this transaction are now tenant scoped
});
The WITH CHECK clause stops a user from writing a row into another tenant. Connect with a role that does not own the tables and is not a superuser, because those bypass row security.
Apply the same thinking outside the database: tenant prefixed cache keys, object storage paths, search indexes, queues, background jobs and log access.
How to test it
- Create tenants A and B with data in each.
- As a user in A, request B's object IDs on every endpoint: read, update, delete, export, file download.
- Try to create or move an object with B's
tenant_idin the body. - Run background jobs and confirm they carry the tenant context.
- Check that cache keys and file paths include the tenant.
Common mistakes
- Taking
tenantIdfrom a header or body field. - Forgetting reports, exports and admin tools.
- Sharing cache keys across tenants.
- A database role that bypasses row security.
- Unique IDs that are guessable, used as the only protection.
Quick check
Is row level security enough on its own?
It is a strong backstop, but application checks, cache keys, storage paths and jobs need tenant scoping too.