Multi-tenant security: keep each customer's data apart
Multi-tenant security is the work of keeping each customer's data apart when they all share one app, one codebase and often one database. It is what most SaaS products run on, and it has a sharp edge: one missing check can show one customer's invoices to another. This guide follows the OWASP Multi-Tenant Application Security Cheat Sheet through the places that boundary breaks, and ends with a two-tenant test you can run on your own app.
Multi-tenant security: where one customer's data meets another's
A tenant is one customer account: a company, a team or a workspace. OWASP's point is blunt. In a shared system, a single bug can expose every tenant's data, and a single misconfiguration can leak data across the boundary. Its list of key risks includes:
- Cross-tenant data leakage, from bugs or settings that show one tenant's data to another.
- Insecure direct object references, where changing an ID in a request reaches another tenant's record.
- Tenant context injection, where a tenant ID in a request, token or header is trusted.
- Shared resource poisoning of caches, queues or storage.
- Noisy neighbors, where one tenant uses up shared capacity and slows the rest.
- Insecure offboarding, where access or data outlives the customer relationship.
Most of these are a kind of broken access control, with one extra question on every request: which tenant is this, and who said so?

Five places the tenant boundary breaks. Simplified from the OWASP Multi-Tenant Application Security Cheat Sheet.
Take the tenant from the server, never from the request
OWASP's first rule is about where the tenant comes from. Establish tenant context early in each request, in middleware, and bind it to a server-verified identity and that identity's current membership in the tenant.
A tenant ID the client sends, in a header, a URL such as /acme/invoices or a JSON field, is only a selector. It says which tenant the user wants, not which tenants the user may use. The server must check that the signed-in user belongs to the selected tenant before anything else runs. Random, hard-to-guess IDs help against enumeration, but the cheat sheet is clear that they are not an authorization control.
Once verified, pass that tenant context to every component that needs it, and do not let a later component swap it for an unverified value. OWASP also warns against skipping authorization just because a service is internal.
Check the tenant on every lookup
The most common leak is a lookup by record ID alone: GET /invoices/8812 returns invoice 8812 whoever owns it. For each tenant-owned resource, OWASP says to verify that the user can act in that resource's tenant, and to include the tenant in the lookup or the authorization policy. A composite key of tenant ID and resource ID is one way, not the only one.
Where you put the check matters as much as the check. Put it at a boundary that every path to tenant data crosses, such as one data access layer, rather than in each route handler, and add database checks underneath as defense in depth. That way a developer writing a new endpoint cannot forget the filter. The same applies endpoint by endpoint in a REST API security checklist.
Row-level security in Postgres, without the bypass
If your tenants share tables in PostgreSQL, row-level security (RLS) lets the database itself refuse rows from other tenants. The cheat sheet and the PostgreSQL manual together give the details that catch teams out:
- Some roles skip RLS entirely. PostgreSQL says superusers and roles with
BYPASSRLSalways bypass row security, and table owners normally do unless the table hasFORCE ROW LEVEL SECURITY. OWASP says the normal request path must connect as a least-privileged role that is neither. Check the role that is actually deployed, usingrolsuperandrolbypassrlsinpg_roles, not only what your config file says. - Set the tenant per transaction. Begin a transaction, set the tenant with
SET LOCALorset_config('app.current_tenant', tenant_id, true), run the queries, then commit or roll back before the connection goes back to the pool. Never assume a borrowed connection has the right setting. - Fail closed. If the tenant context is missing, refuse; never fall back to an unscoped query. In a policy,
current_setting('app.current_tenant', true)returns NULL instead of an error when the setting does not exist, and a comparison with NULL matches no rows. - Prove coverage. List the tables that hold tenant data and compare them with
relrowsecurityandrelforcerowsecurityinpg_classand withpg_policies. A new table with no policy should fail a test.
Caches, files and background jobs
RLS only guards the database. OWASP asks you to classify every cached value, stored file and queued job as global, tenant-scoped or user-scoped, then:
- Caches. Put the tenant in every cache key whose value varies by tenant, plus anything else that changes the result, such as the user or their permissions. Give shared entries an explicit global namespace. Key separation does not replace authorization: check access before reading a protected cached value.
- Files. Partition tenant files with a tenant-aware key, bucket or storage policy. Authorize the exact object before serving it or creating a signed URL, and limit the URL to that object and method.
- Background jobs. Take the tenant from the authenticated producer and bind it to the message. A tenant ID in a queued message is not proof on its own; the worker re-establishes the context and authorizes the operation, re-checking membership if the job ran much later.
Shared capacity, logs, offboarding and restores
Isolation also covers availability. When tenants share capacity, add the tenant as one dimension of your rate limits, next to the global, user and IP limits you already have, and bound each tenant's share of queues, worker pools and database connections. The same idea, applied to clients, is in limiting what one client can use.
Log the server-verified tenant in security events, and alert on denied or unexpected cross-tenant access. When a customer leaves, follow a written retention and deletion policy across live stores, caches, object versions, replicas, exports and backups. When restoring one tenant, never restore a shared snapshot over live data; check ownership before copying records back, and test a restore with two tenants.
Worked example: a two-tenant test
Set up two test tenants, A and B, each with one user and a few records of every kind: an invoice, an uploaded file, something cached on a dashboard. Then run these checks, and keep each one as an automated test once it passes.
- Swap IDs. Signed in as A, call each endpoint with an ID that belongs to B. Every call should return not found or forbidden.
- Inject the tenant. Signed in as A, send B's tenant ID in a header, the URL and the request body. The server should refuse, because A is not a member of B.
- Check the role. In the deployed database, confirm the app's role has neither
rolsupernorrolbypassrls. - Reuse connections. Send requests for A and then B over the same pooled connections, and confirm B never sees A's tenant setting.
- Look outside the database. As A, open B's file links, dashboards and exports. None should load.
- Restore once. Restore tenant A from a backup in a test environment and confirm none of B's records or files appear.
If a check fails, fix it at the shared boundary, not in the one endpoint where you saw it, then run all six again.
Frequently asked questions
Is a tenant ID in the URL safe?
It is fine as a selector. OWASP says the server must still verify that the signed-in user belongs to that tenant; the ID itself proves nothing.
Is row-level security enough on its own?
No. RLS guards database rows, but caches, files, queues and backups sit outside it, and a superuser or BYPASSRLS role skips it. Use it as a layer under tenant-aware application code.
Do random IDs stop cross-tenant access?
No. The cheat sheet treats opaque or random identifiers as defense in depth against guessing, not a substitute for checking the tenant on each lookup.
Do I need a separate database for each customer?
Not always. OWASP asks for an isolation boundary that fits the data and the threat, and notes that separate keys or credentials can add strength where the risk calls for it.
Get started
Pick one endpoint today and run the ID swap test. If you want an outside review of your tenant boundaries, whitehatstoic's cybersecurity and AI safety testing covers web app and API review, with a written report, fixes and a retest after fixes. If you are building the product, whitehatstoic's full-stack development works in Next.js, Node and Postgres. No test can promise to find every weakness; the work is scoped after a short call.

Building something that has to be safe? Book a meeting with whitehatstoic: tell us the product, the deadline and your biggest worry, and we reply with a plan and a price.
Comments
No comments yet.
Sign in or make an account to comment.