Tenant isolation testing for multi-tenant SaaS

Tenant isolation testing checks that one customer of a SaaS product cannot read or change another customer's data. Excession Labs runs the same actions from two test tenants, across every object type, search, export, cache and file path. A cross-tenant effect counts only when the other tenant's session reads it back. The report includes controls that held.

Tenant isolation testing checks that one customer of your SaaS product cannot read or change another customer's data. Excession Labs tests this boundary with two test tenants, and we report a leak only when we can prove it.

A tenant leak is broken access control, which is A01 in the OWASP Top 10:2025. At object level it is the risk OWASP lists as API1:2023. It is also the finding an enterprise buyer's security team looks for first.

How does the two-tenant test work?#

We ask for two test tenants that are as alike as possible: same plan, same roles, same feature flags. Each tenant needs at least a member and an admin. In every feature we create a record with a readable marker, such as A-note-001 in tenant A and B-note-001 in tenant B.

Then we run each in-scope action from tenant A with tenant B's identifiers, and the reverse, once per role and once anonymously. If tenant A's response contains B-note-001, the leak needs no interpretation. The multi-tenant isolation testing guide has the full 12-area matrix and a worked request, so this page covers only what the service includes.

What do we test?#

We test every place where the server could return or change data without checking the tenant. This table shows the areas and what we try in each.

Area What we try Proof of a leak
Object types Fetch, change and delete B's projects, invoices, users, API keys and webhooks by ID from A B's marker in a response, or a changed B record
Lists and filters Change tenant_id, org or account parameters and headers B's rows in a list
Search Search, autocomplete and "similar items" for B-note B's records in results
Exports and reports Start an export in A, then request B's export or report ID B's file content
Files and images Open B's file URL with no token, and with A's token B's file bytes
Caches Request the same URL from A and B in turn One tenant sees the other's cached body
Background jobs Trigger a job in A with B's resource ID B's data in the job output
Roles and admin paths Call admin routes of tenant B as a member of A An admin action succeeds
Invitations and billing Invite into B's org, read or change B's plan and seats A new member in B, or a changed plan in B

Where do search, export and cache paths break?#

They break where the main query filters by tenant and a second code path does not. App code often checks the tenant on the main table and forgets it in the search index, the export worker or the cache key.

For search, we test every query type, because an index can hold all tenants' documents with the filter applied to only some of them. For exports, we start a job as A and then fetch B's job and file IDs, since a worker may run with an identity that can read every tenant. For caches, we request the same path as both tenants and compare bodies. A cache key built from the URL alone can serve one tenant's page to the other.

For files, we check whether a storage URL works without a token or with another tenant's token. A guessable or leaked key is a finding even when the application interface hides it.

What do row-level security pitfalls look like?#

Row-level security (RLS) moves the tenant check into the database, but it fails in a few ways that we test for. The PostgreSQL documentation describes the rules behind each one.

  • The application's database role owns the table. Table owners skip the policy unless the table uses FORCE ROW LEVEL SECURITY.
  • The role has the BYPASSRLS attribute, which skips every policy.
  • The tenant setting comes from a header or body field that the client controls, not from the verified session.
  • Some tables have a policy and others, such as join tables, do not.

We cannot read your database policies in a black-box test. We find these faults through behavior: a request that should return nothing returns B's rows. If you give us the schema or the policy files, we check them as well.

What counts as proof?#

A finding needs evidence of a real effect. For a read, the response contains the other tenant's marker. For a write, we change only a test-owned record in tenant B and read it back through B's own session. A bare "200 OK" is not proof.

Every result runs next to controls: A's own ID, a nonexistent ID, a wrong record type, another account's token and an anonymous call. We save requests and responses with timestamps. Destructive proofs use throwaway records created for the test, and the scope names them.

Sample cross-tenant finding

This is a demonstration. It uses a fictional app, Harbor Notes Teams, and no real customer system was tested.

Finding T-02: The export download ignores the tenant (High). A member of tenant t_acme requests the download of an export job that belongs to t_globex.

GET /api/exports/exp_9021/download HTTP/1.1
Host: app.example.com
Cookie: session=<acme-member-session>

The response held rows with the marker B-note-001 (Observed, and tenant B's own session lists the same rows). Controls: exp_00000 returned 404, and an anonymous call returned 401. Fix: resolve the export by tenant and ID together, in one query.

What do you receive?#

You receive a written report in HTML, PDF and Markdown, with proof-of-concept scripts. It has an executive summary with priority actions, a findings table, and one section per finding: description, impact, verified results, reproduction and fix. A "Controls that held" section lists the isolation checks that passed, which is the part a buyer's security team asks to see. It also lists limitations and open questions.

Open the sample report to see the format before you buy. We can retest fixed findings (see retesting). For general SaaS scope, such as roles, billing and integrations, see SaaS penetration testing.

What does it cost?#

We publish no prices. We quote after a short call and a written scope. The number of tenant-scoped object types, roles, integrations and API routes drives effort. Pricing lists every factor.

To scope a tenant isolation test, request a pentest. Small teams can apply for a free pilot.

Questions

Is tenant isolation testing the same as multi-tenant penetration testing?

They overlap. Tenant isolation testing focuses on the boundary between customers. A multi-tenant penetration test also covers the rest of the application, such as sessions, roles and business logic. We scope either one, or both together.

Does a database per tenant remove the need for this test?

No. Caches, queues, file storage, search indexes and admin tools can still be shared between tenants. We test each shared layer whatever the database design.

What do you need to start?

Written authorization from the system owner, two test tenants with test data, at least two roles in each tenant, and a route list or API description. We agree a safe channel for credentials during scoping.

Do you test with real customer tenants?

No. We use test-owned tenants and records. If production is the only option, the written scope says so and sets limits on destructive actions.

Sources

  1. OWASP Top 10:2025, A01 Broken Access Control (checked 9 November 2026)
  2. OWASP API1:2023 Broken Object Level Authorization (checked 9 November 2026)
  3. OWASP Multi-Tenant Application Security Cheat Sheet (checked 9 November 2026)
  4. PostgreSQL documentation: Row Security Policies (checked 9 November 2026)

Need a pentest you can act on?

Tell us what you need tested and why. We reply to agree scope, and testing starts only after written authorization.