Home › Blog › Multi-tenant isolation
Multi-tenant isolation: how IDOR leaks data between your SaaS customers
October 12, 2026 · ~9 min read
Almost every SaaS is multi-tenant: all your customers live in the same database, separated by a tenant_id (or user_id, org_id). Isolation between them depends on one check on every access. Forgetting a single one is the #1 flaw in the OWASP API Top 10 — IDOR / BOLA — and it's the one most likely to turn a new SaaS into bad news. This is the guide to the flaw: why it happens, the 7 patterns, and where the check has to live.
IDOR / BOLA in one sentence
IDOR (Insecure Direct Object Reference) and BOLA (Broken Object Level Authorization) are the same problem: the app uses an identifier supplied by the client to fetch an object, but doesn't confirm that object belongs to the requester. Authentication (who you are) is correct; object-level authorization (can you see this one?) is missing.
1. Authorizing by "logged in", not "is owner"
The classic pattern. It passed auth, so it reads any object:
app.get('/invoices/:id', auth, async (req, res) => {
const inv = await db.invoices.findById(req.params.id);
res.json(inv); // ❌ never checks inv.tenantId
});
// ✅ the lookup is already scoped to the session's tenant
const inv = await db.invoices.findOne({
id: req.params.id, tenantId: req.session.tenantId
});
if (!inv) return res.sendStatus(404);
2. Trusting a tenant_id the client sends
If the tenant comes from a query param, header or request body, the client just picks another and crosses the boundary. The tenant must come from the server-side session/token, never from the request.
// ❌ client picks the tenant const tid = req.query.tenantId || req.body.tenantId; // ✅ tenant comes from the verified token const tid = req.session.tenantId;
3. Thinking a random ID (UUID) is security
Swapping sequential IDs for UUIDs makes them harder to guess, but that's not authorization. IDs leak in URLs, logs, emails, referrers and API responses — the attacker rarely needs to guess. UUID is defense in depth; the ownership check is still mandatory.
4. Forgetting the scope on list endpoints
Worse than leaking one object is leaking the collection. A list route without the tenant filter returns the whole base at once.
// ❌ lists everything from everyone const rows = await db.orders.find({}); // ✅ scope is mandatory const rows = await db.orders.find({ tenantId: req.session.tenantId });
5. Nested resources without re-checking the chain
On /projects/:pid/tasks/:tid it's common to check the project and trust the task — then someone passes a tid from another project/tenant. Validate the whole chain: the task belongs to the project, and the project belongs to the tenant.
const task = await db.tasks.findOne({
id: tid, projectId: pid
});
if (!task || project.tenantId !== req.session.tenantId) return res.sendStatus(404);
6. Cache, storage and files without the tenant in the key
Isolation isn't just the database. A cache key user:settings without the tenant serves the wrong customer's data; a file path /uploads/invoice.pdf without the tenant lets one download another's. Put the tenant in the key of everything shared.
cache.get('settings') // ❌ shared across tenants cache.get(`t:${tid}:settings`) // ✅ isolated per tenant
7. Jobs, exports and webhooks without scope
Scheduled reports, CSV exports, reprocessing and webhook handlers run outside the request context — and it's easy for them to forget the tenant, mixing customers' data into one file or email. Every background process has to carry and respect the tenant explicitly.
Where the check has to live
Defense in depth: enforce isolation at the data layer — Supabase RLS or Firebase rules, which hold even if the code forgets — and at the application layer (the per-tenant scope on every query). One doesn't replace the other: RLS is the safety net; the app check is the door.
403/404 is a leak. Worth automating this in an integration test that runs in CI. Why pattern tools miss it: see SAST and what Semgrep misses.
Sure one customer can't see another?
SAAS SHIELD reviews your repository looking for exactly this — object-level authorization, tenant scope and RLS/rules — and returns every flaw with its location, evidence and fix.
Request an audit →