Learn / Security / Lesson 04

Access control and tenant isolation

The most common serious web flaw is not clever hacking but a server that forgets to ask "is this yours?". How to make that question impossible to skip.

Last updated: 2026-09-19

What it is

Authentication answers who are you? Access control answers may you touch this? In a multi-tenant system, where many businesses share one app and one database, the second question has a third part: does this record belong to your business?

Broken access control has been number one on the OWASP Top 10 since 2021. It needs no exploit, only a changed number in a URL.

Why it is a rule

In each case the data was served to anyone who asked for the right id. The server never checked whether the asker should see it.

How to do it

Take the tenant from the server, never from the request

The business a user belongs to comes from their session on the server. A tenant id in the URL, the body or a header is a claim, and claims are not checked by being present.

js
const company = req.session.companyId

const { rows } = await db.query(
  'SELECT * FROM invoices WHERE id = $1 AND company_id = $2',
  [req.params.id, company],
)
if (!rows.length) return res.sendStatus(404)

The tenant filter is in the query, not applied after it. A query that loads first and checks later leaks through every code path that forgets the check.

Deny by default

A new route is closed until someone opens it on purpose. Middleware that requires a session and a role goes in front of the route group, not inside each handler.

Answer 404, not 403

"Forbidden" confirms the record exists. "Not found" tells an attacker nothing.

Random ids help, but are not access control

UUIDs stop people guessing neighbours, which is why First American's sequential numbers made things worse. But an id that leaks in a log or a shared link is still an open door if the server does not check ownership.

Caches are part of access control

A cached answer must be keyed by everything its access check depends on: tenant, role, grant. A cache keyed by URL alone serves one company's data to the next.

Test the wrong tenant

For every read and write route, a test signs in as company A and asks for company B's record. The only passing answer is 404.

How we do it here

Every business's books, sales and bookings live in shared tables with a tenant column, and every query carries the tenant taken from the server-side session. Roles are checked in middleware per route group. Performance work is not allowed to drop a tenant filter or widen a cache key.

Benefits

Disadvantages

Checklist

Sources

Read this lesson as Markdown