---
title: Access control and tenant isolation
summary: 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.
topic: security
order: 4
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

- **First American Financial (2019).** Document links used sequential numbers and needed no
  sign-in. Changing the number in the address bar showed someone else's mortgage papers, bank
  statements and ID documents. About 885 million records going back to 2003 were exposed.
- **Optus (2022).** An internet-facing API returned customer records without authentication.
  Around ten million Australians' details were taken, including passport and licence
  numbers.
- **Parler (2021).** Posts and videos had predictable ids and no rate limit, so the whole
  site was downloaded before it went offline.

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

- One rule, applied everywhere, closes the most common serious flaw on the web.
- Tenant filters in the query also make the database faster: indexes lead with the tenant
  column, so each business's rows sit together.
- Shared tables keep one schema, one migration path and one backup for every client.

## Disadvantages

- Every query is longer, and every new query is a chance to forget the filter.
- Shared tables mean a single missed filter exposes every tenant, not one. A database per
  tenant is safer but far costlier to run and migrate.
- Cross-tenant reporting for the operator needs its own, separately guarded path.

## Checklist

- The tenant comes from the session, never from the request.
- The tenant filter is in the SQL.
- Routes are closed by default and guarded per group.
- Caches are keyed by tenant and role.
- A wrong-tenant test exists for every route.

## Sources

- [OWASP Top 10 — A01 Broken Access Control](https://owasp.org/Top10/A01_2021-Broken_Access_Control/)
- [OWASP — Authorization Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html)
- Brian Krebs, *First American Financial Corp. Leaked Hundreds of Millions of Title Insurance Records*, KrebsOnSecurity, May 2019.
