SaaS Penetration Testing: 8 Checks for Tenant Isolation
Tenant isolation means a signed-in user from one customer cannot read or change another customer’s data. These eight checks show where that boundary tends to break, and what a pass or a fail looks like for each.
This post is the checklist. For a worked example with requests, responses and fixes, read Tenant isolation testing: three gaps beyond IDOR. For the access control checks around it, such as credentials, roles and fields, see API Security Testing: 7 Checks for Access Control Gaps.
Set up two test-owned tenants with a member and an admin in each, so you have four accounts. Create a known record in every feature and give it a readable marker, such as A-note-001 in Tenant A and B-note-001 in Tenant B. Get written permission first, set limits for the test and use synthetic data. (sources: multi-tenant isolation testing guide)
- Tenant context: Change tenant IDs and other scope selectors.
- Object access: Test cross-tenant reads, updates and deletes.
- Lists and search: Check results, filters, counts and pagination.
- Roles: Test member and administrator limits in both tenants.
- Bulk actions: Submit batches that hold IDs from both tenants.
- Files and exports: Check downloads, storage references and signed links.
- Background jobs: Follow tenant scope through workers, retries and callbacks.
- Caches: Check whether a shared cache returns another tenant’s data.
A 200 OK is not proof of a failure, and it is not proof of a pass. Read the returned data, read back every change from the owning tenant, and save the request and response pairs. Retest each fix from both tenants.
Checks 1 to 3: Tenant Context, Objects and Search
1. Change the Tenant Context
Sign in as a Tenant A user. Replace every value that selects a tenant with Tenant B’s value: path segments, query parameters, headers, cookies, body fields and token claims. Test each selector alone before you combine them, then repeat from Tenant B. Add three controls: Tenant A’s own value, a tenant that does not exist, and Tenant B’s value with no token.
A pass means the server takes the tenant from the verified session or token and ignores or rejects the value the client sent. A token with changed claims must be rejected. A failure is a Tenant A request that returns Tenant B’s markers or metadata, or one that moves Tenant A into Tenant B’s scope. (sources: multi-tenant isolation testing guide, methodology, OWASP Multi-Tenant Security Cheat Sheet)
2. Test Cross-Tenant Object Access
Put Tenant B’s IDs, slugs or UUIDs into Tenant A’s read, update and delete requests. Run controls next to them: Tenant A’s own object, an ID that does not exist, and a request with no authentication. The controls show whether a denial comes from the ownership check or from some other error.
A read passes when it returns 403 or 404 with no Tenant B data. A read fails when Tenant B’s marker appears. For updates and deletes the status code proves little, so read the object again through Tenant B’s session to see whether the change or deletion took effect. A random ID does not replace the ownership check. The IDOR walkthrough has a local example of this. (sources: IDOR testing guide, OWASP API1:2023 Broken Object Level Authorization)
3. Check Lists and Search
Change or remove the tenant filter on lists, search, autocomplete, filters, pagination, counts and facets. Test search apart from direct object access. A blocked object route does not show that the search index filters by tenant, because an index can hold every tenant’s documents and apply the filter in only some query types.
Foreign rows, snippets, names or IDs are a failure. To test counts, change only test data in Tenant B, then run Tenant A’s query again. A count that moves when only Tenant B changed shows exposure. A pass means Tenant A’s results hold Tenant A’s data before and after the change. (sources: multi-tenant isolation testing guide)
Checks 4 to 6: Roles, Bulk Actions and Files
4. Test Role Boundaries
Test every role the app exposes, in both tenants. Replay Tenant A’s administrator requests for users, roles, billing, settings and audit logs against Tenant B’s records. Then check what a member cannot do inside their own tenant. For invitations, check whether the invited address really became a member of the other tenant. Vertical limits inside one tenant are covered in broken access control.
An administrator in Tenant A is not an administrator in Tenant B. A pass is a denial for every Tenant B record. A failure is any Tenant B record that Tenant A’s administrator can read or change, confirmed through the owning tenant’s session. (sources: multi-tenant isolation testing guide)
5. Test Mixed-Tenant Bulk Actions
Send batches that mix IDs from both tenants to bulk update, delete, invite and read routes. Also try duplicate IDs, an empty list and the largest batch the agreed limits allow. One ID in a list can skip a check that the single-item route enforces.
A pass is a batch that is rejected as a whole, or one that processes only the items the caller owns, as the endpoint’s documented rules say. A failure is any Tenant B record that changes or appears in the result. Read the per-item results, then read both tenants. A success response alone proves nothing. (sources: IDOR testing guide)
6. Check Files and Exports
Using Tenant A’s session, request Tenant B’s file IDs, storage keys, export job IDs and download tokens. Test download and export routes apart from the record routes, because leaks often sit outside the main query path. Check signed links against their documented sharing rules: does a changed object reference in a signed URL, or a reused token, reach beyond what the link was issued for? Repeat with no authentication where it applies.
A pass is a 403 or 404 for Tenant B’s files. A failure is Tenant B’s marker in file bytes, metadata or export rows. A successful download status is not enough on its own. A link made to be shared may work for anyone who holds it, and that is a design choice, not an isolation failure. Access to another tenant’s content outside the link’s rules is a failure. (sources: multi-tenant isolation testing guide)
Checks 7 and 8: Background Jobs and Caches
7. Test Background Jobs and Callbacks
Put markers in both tenants, then follow them through jobs, retries and callbacks. From Tenant A’s session, swap in Tenant B’s job IDs, resource IDs and tenant references when you create a job, check its status and fetch its result. Point callbacks and webhooks only at a receiver you control. Log the starting request, job ID, retry history, destination and payload.
A pass means each job keeps the tenant that started it, even when the worker runs under a service identity that can reach every tenant. A failure is status, output, a notification or a webhook payload from Tenant B arriving in Tenant A’s session or at your receiver. Check any write made without permission through the owning tenant’s session. (sources: multi-tenant isolation testing guide)
8. Check Cache Isolation
Request the same path with the same parameters as Tenant A, then as Tenant B, then in the reverse order. Repeat across application, API, search and content-delivery caches. Where a response differs by user or role, vary those too.
A cache hit alone is not a failure. A pass means each tenant gets its own data, and the cache key holds the tenant plus any user or role the response depends on. A failure is a hit that returns another tenant’s records, markers or sensitive metadata. Record both requests, the timestamps, the response bodies, any cache header such as X-Cache, and the ownership fields in the body. Our tenant isolation post reproduces a cache leak with real requests. (sources: multi-tenant isolation testing guide, OWASP Multi-Tenant Security Cheat Sheet)
Record Results, Fix and Retest
Give each of the eight checks a verdict: passed, failed or not tested. A test that was blocked or left incomplete is not a pass. For each failure, log the tenant pair, caller role, endpoint, time and exact steps, with sanitized requests and responses and the marker or before-and-after state that crossed the boundary. Label what you observed separately from what you infer, such as the cause, and list the controls that held. (sources: methodology, multi-tenant isolation testing guide)
Fix the boundary that failed. Take the tenant from the verified session, and enforce it at the data layer where you can, for example with row-level security in Postgres. Check each item in a bulk request. Check permission when a file is delivered. Give workers a per-tenant scope. Put the tenant in every cache key. Then repeat the original requests and controls from both tenants, test the sibling routes, and confirm that permitted access still works. (sources: multi-tenant isolation testing guide, IDOR testing guide)
FAQs
How often should I test tenant isolation?
Run the checks on every release that adds a new object type, export or integration, and again after each fix. One fixed route does not clear its siblings: a route can check ownership while another route to the same object does not. (sources: multi-tenant isolation testing guide, IDOR testing guide)
Can I test tenant isolation in production?
Yes, when the tests use only test-owned tenants and the written scope says so. A staging copy with synthetic data is often the better choice. Never test with real customer tenants. Agree limits in writing for anything that sends email or calls a paid third-party service. (sources: multi-tenant isolation testing guide, methodology)
Where are tenant isolation gaps most often missed?
In the shared layers outside the main query path: caches, search indexes, file storage, background jobs and exports. Endpoints that accept a tenant ID from the client are the other common place. The tenant filter is often present on the main table and missing in those layers. (sources: multi-tenant isolation testing guide, OWASP Multi-Tenant Security Cheat Sheet)