Tenant Isolation Testing: How to Prove Tenant Boundaries Hold

A software engineer merges a pull request adding a batch update endpoint for project milestones. The unit tests pass, integration tests confirm records update as expected, and your static analysis tool flags zero CVEs. Three weeks later, a customer notices their dashboard displays another company's sprint goals. The endpoint accepted a milestone ID from the request body without checking whether that milestone belonged to the requesting user's organisation.
This failure mode is common across multi-tenant architectures. Multi-tenancy relies on software discipline rather than physical separation. When that discipline slips, data leaks. Traditional security scanners miss these issues because tenant isolation is an authorisation and data scoping problem, not a malformed payload or a vulnerable library.
Preventing cross-tenant data leaks requires tenant isolation testing: a structured, repeatable methodology to verify that tenant boundaries are enforced across API endpoints, background workers, and data stores. This guide covers how to build, run, and automate cross-tenant security tests, along with the prerequisites needed to make them part of your engineering workflow.
Step 1: Define Your Tenant Boundary and What Crossing It Looks Like
Before writing test scripts, map the exact mechanism that establishes tenant identity in your application. In modern SaaS applications, tenant identity may be established through a verified JWT claim, a server-side session, an API key linked to a tenant record, or another application-specific authentication mechanism. Whatever the mechanism, the application must validate the identity and enforce the appropriate tenant-level permissions on each protected operation.
In PostgreSQL or MySQL systems with shared tables, tenant isolation commonly relies on tenant-aware query filters, ownership checks, or relationships that establish which tenant owns each record. A tenant_id column is a common implementation, but it is not necessary or appropriate for every table. If your team uses PostgreSQL Row-Level Security (RLS), database policies can enforce row-level access restrictions. These policies may use application-defined session settings, such as app.current_tenant_id, provided the application sets them securely and the policies enforce the intended tenant boundaries.
Crossing a boundary takes three common forms:
- Insecure Direct Object Reference (IDOR): An authenticated caller from Tenant A supplies an entity identifier belonging to Tenant B (for example,
GET /api/v1/invoices/inv_tenant_b_9918) and receives a 200 OK with Tenant B's data. - Blind Writes and State Changes: Tenant A cannot read Tenant B's resource, but can mutate or delete it. Sending a
DELETE /api/v1/webhooks/wh_tenant_b_4412returns a 204 No Content because the SQL statement wasDELETE FROM webhooks WHERE id = $1instead ofDELETE FROM webhooks WHERE id = $1 AND tenant_id = $2. - Parent-Child Relationship Hijacking: An endpoint creates a child entity (such as a task) under a parent entity (such as a project). The handler verifies that Tenant A owns the project, but fails to check whether an assigned tag ID or attachment ID belongs to Tenant B. The child record is created, linking Tenant B's private assets to Tenant A's project.
Document these definitions across your engineering team. A test failure is not merely an HTTP 500 or an unhandled exception. Any response that discloses information another tenant is not authorised to access, reveals the existence of a protected resource where that information is intended to remain private, or modifies a foreign record without authorisation is a failed boundary check.
Step 2: Seed Two Real Tenants With Controlled, Identifiable Data
Do not rely solely on mocks, in-memory stubs, or synthetic unit test fixtures that bypass your middleware when testing tenant isolation. Unit tests can verify individual authorisation decisions, but integration tests should also exercise the actual authentication and tenant-enforcement mechanisms used by your application.
Spin up a dedicated, isolated test database containing two complete, distinct tenants: Tenant Alpha (the attacking tenant) and Tenant Bravo (the target tenant). Both tenants must have identical data models populated with recognisable, deterministic values.
Structure the seed data with clear markers. Every string field should carry a recognisable tenant prefix:
- Tenant Alpha project name:
alpha_project_public_01 - Tenant Bravo project name:
bravo_project_private_99 - Tenant Bravo secret key:
canary_bravo_secret_token_x89
Ensure that both tenants have test records covering the relevant relationships and resource types in your schema. If your SaaS platform supports workspaces, projects, teams, billing accounts, and audit logs, seed representative records for each tenant wherever tenant isolation applies. When Tenant Alpha interacts with an endpoint, your assertions should check response bodies, relevant headers, and database side effects for unauthorised access to Tenant Bravo's canary data, distinguishing genuine cross-tenant exposure from legitimate shared data.
Also create multiple user roles within each seeded tenant. Tenant isolation failures can intersect with broken role-based access control (RBAC). Seed an admin, an editor, and a read-only viewer for both Tenant Alpha and Tenant Bravo. This lets you test both whether users can access resources belonging to another tenant and whether users can perform only the actions permitted by their own roles.
Step 3: Build a Cross-Tenant Access Matrix for Every Endpoint
Tenant isolation testing requires an access matrix that pairs every authenticated route with an unauthorised foreign context. Standard integration tests check that Tenant Alpha can access Tenant Alpha's resources. Cross-tenant testing asserts that Tenant Alpha explicitly fails when attempting to read, mutate, or reference Tenant Bravo's resources.
Your test suite should execute four primary assertions for every route:
- Direct Read (GET): Send a request authenticated as Tenant Alpha to fetch Tenant Bravo's resource ID. The response must not disclose the protected resource or its contents. Expect HTTP 403 Forbidden or HTTP 404 Not Found according to your application's authorisation and information-disclosure policy. Returning 404 can help avoid revealing whether an inaccessible resource exists, but the chosen status code alone does not establish that tenant isolation is working.
- Cross-Tenant List Pollution (GET): Authenticate as Tenant Alpha and call the collection endpoint (
GET /api/v1/projects). Verify that the returned list contains only records Tenant Alpha is authorised to access, according to the application's tenant and sharing rules. Check the serialised payload for unauthorised disclosure of Tenant Bravo's canary values. - Cross-Tenant Mutation (PUT/PATCH/DELETE): Authenticate as Tenant Alpha and send update payloads targeting Tenant Bravo's record IDs. Assert that the request does not make unauthorised changes to Tenant Bravo's records or their protected relationships. Verify that relevant fields, foreign keys, timestamps, and revision numbers remain unchanged where applicable, while allowing explicitly intended audit logs and operational side effects.
- Cross-Tenant Association (POST): Attempt to create a new resource under Tenant Alpha that references a resource ID belonging to Tenant Bravo in a foreign key or relationship field. The request must be rejected according to the application's authorisation and error-handling policy. It must never commit an unauthorised record linking Tenant Alpha's tenant ID to Tenant Bravo's resource ID.
Static analysis tools can struggle to identify these flaws when the code looks structurally sound: the route authenticates the token, maps the request to an ORM model, and runs a valid query, but does not enforce the required tenant boundary. As we explore in our analysis of SAST and business logic vulnerabilities, static analysis can detect some authorisation weaknesses, particularly when the relevant rules are explicit, but may lack the application-specific context needed to recognise others. Executing a cross-tenant test matrix against the application helps establish whether these missing checks are exploitable at runtime.
Step 4: Test Non-API Paths (Queues, Workers, Caches and Exports)
Web APIs are not the only part of the attack surface in modern SaaS applications. Tenant-isolation weaknesses can also arise in asynchronous message queues, background workers, shared caches, and batch export jobs, where tenant context and authorisation checks may not be enforced consistently.
Background workers (such as Sidekiq, Celery, or BullMQ) can introduce tenant-isolation risks if they fail to preserve or re-establish the required authorisation context. An API endpoint may accept a job, extract the payload, and push a message onto a Redis queue. If the payload contains only { "task_id": "tsk_102" } and the worker does not independently establish the authorised tenant context or verify access to the referenced task, it may process the job with excessive privileges. Test whether an attacker can manipulate a task ID or job payload to trigger operations across tenant boundaries.
Test these paths by monitoring four specific non-API interfaces:
- Redis and Memcached Key Namespaces: Inspect your caching layer during test runs. Ensure every cache key for tenant-specific data or tenant-dependent query results includes the appropriate tenant identifier in its namespace (for example,
cache:org_123:projects:456instead ofcache:projects:456). If key generation omits the tenant scope where one is required, test whether Tenant Alpha can read or overwrite cached data belonging to Tenant Bravo. Key namespacing should complement, not replace, authorisation checks. - Asynchronous Job Arguments: Write tests that enqueue background jobs directly using Tenant Alpha's credentials, supplying Tenant Bravo's entity IDs. Verify that the worker rejects the job with an authorisation error and does not execute it.
- File Storage and Presigned URLs: When an endpoint generates an S3 presigned URL for document downloads or avatar uploads, verify that the application authorises access to the exact object and operation before generating the URL. Test whether an authenticated request from Tenant Alpha can generate a presigned read URL for an object belonging to Tenant Bravo. Tenant-aware storage paths, buckets, and policies can provide additional isolation, but the storage design must enforce the intended access restrictions rather than relying on object naming alone.
- CSV and PDF Export Generators: Export endpoints can introduce tenant-isolation risks if their queries fail to enforce the appropriate access restrictions, including when they use raw SQL instead of standard ORM methods. Write automated tests that trigger exports and parse the resulting files, checking for unauthorised canary values belonging to other tenants.
Step 5: Run Isolation Tests in CI on Every PR That Touches Auth or Data Access
Relying solely on manual isolation tests before a major release can delay feedback and leave defects undetected until late in the development cycle. Automated isolation tests belong in your continuous integration (CI) pipeline. However, running a full cross-tenant matrix against hundreds of endpoints can increase pipeline runtimes, so test scope and execution strategy should be designed around the coverage and feedback speed your team needs.
To balance test coverage with deployment velocity, introduce path filtering and code classification in your CI workflows. Trigger the full cross-tenant matrix whenever a pull request modifies files matching critical paths:
- Database migrations and schema definitions
- Middleware and authentication handlers
- Data access layers, repositories, or ORM base models
- GraphQL resolvers or API router configurations
To keep pipeline runtimes manageable, run your test runner against a lightweight PostgreSQL service container spun up directly in CI. Apply migrations, run the seeding script for Tenant Alpha and Tenant Bravo, and execute independent parts of the cross-tenant suite in parallel where practical. Measure the actual runtime in your CI environment and adjust the test scope, parallelisation, and infrastructure to meet your team's target without sacrificing meaningful coverage.
Alongside automated test suites, consider security reviews during the pull request phase. Borg provides PR Security Reviews designed to assess code changes before they merge. These reviews can help identify potential security issues involving authentication, access control, and sensitive APIs in the context of the surrounding repository. Where a review identifies a missing tenant check or an unscoped query, developers can investigate and address the issue before the change progresses through the deployment pipeline. The effectiveness of this approach depends on the review's coverage and the accuracy of its findings.
Step 6: Verify the Fix by Retesting After Every Patch
When a cross-tenant vulnerability is discovered, a patch may fix the immediate symptom without addressing the root cause. For example, an engineer might add a manual tenant check to an endpoint's controller while leaving the underlying repository query unscoped, allowing the same issue to persist through another access path.
Every patch addressing an isolation failure requires three verification steps:
- Add a Dedicated Regression Test: The pull request containing the fix must include a new test case within the cross-tenant test suite. This test must authenticate as Tenant Alpha, target the exact endpoint and parameter combination that caused the leak, and assert that Tenant Bravo's data remains inaccessible.
- Verify Secondary Query Paths: If the leak occurred on an update endpoint, inspect every other endpoint interacting with the same underlying database table. If
PATCH /api/v1/invoices/:idwas missing atenant_idscope, check whetherGET /api/v1/invoices/:idorPOST /api/v1/invoices/:id/refundshare the same flawed query pattern. - Retest the Running Application: Automated unit or integration tests in CI provide evidence that a fix works along the code paths they exercise, but they may not cover every relevant scenario. Where appropriate, verify the fix against a running deployment using targeted security tests. Platforms like Borg also provide automated retesting: when a pull request linked to a finding merges, Borg reviews the merged code change against the original finding to assess whether the root cause has been addressed. This helps connect identification and remediation, but a code review is not the same as retesting the running application or confirming that the fix has been deployed. You can learn more about this process in our breakdown of how automatic retests verify a fix.
What These Tests Won't Catch (and What to Do About It)
Writing integration tests against a cross-tenant access matrix solves the most obvious category of multi-tenant bugs: straightforward IDORs and missing WHERE clauses. But deterministic matrix tests have limits. They only test the endpoints and query structures you explicitly program them to test.
Deterministic CI tests can miss four advanced tenant isolation failure modes if the relevant scenarios are not explicitly covered:
- Complex Multi-Step Exploit Chains: An attacker may not simply call an endpoint with another user's ID. They trigger a state transition in one service, exploit an asynchronous race condition in a queue, and retrieve leaked data through a third unrelated endpoint.
- Context Leakage in LLM and RAG Pipelines: If your SaaS platform uses retrieval-augmented generation (RAG), tenant isolation depends on how access restrictions are enforced throughout the retrieval pipeline. Controls may include vector database metadata filtering, separate indexes or collections, and application-level authorisation. An incorrectly scoped retrieval query can pull another tenant's private documents into the LLM context window. Test both retrieval results and generated outputs to determine whether tenant data can cross boundaries, including through endpoints covered by your existing API test matrix.
- Protocol and Header Manipulation: Integration tests may not cover every way that HTTP headers and protocol handling can affect routing or authorisation. Depending on the architecture, attackers may exploit incorrectly trusted
X-Forwarded-Hostheaders, inconsistent handling of hop-by-hop headers, or HTTP request smuggling to interfere with request processing. Test these behaviours where relevant to your infrastructure, and verify that proxy and routing configurations cannot bypass tenant-level access controls. - Blind Side Effects and Timing Leaks: Even if an API returns HTTP 404, differences in processing time or observable side effects may reveal whether a resource ID exists in another tenant's account. Standard matrix integration tests may not detect subtle timing or side-channel leaks unless they are specifically designed to measure them. Where resource-existence disclosure is a realistic risk, test whether differences are repeatable and exploitable under realistic conditions.
Bridging this gap requires continuous offensive testing. A yearly pentest report can become outdated as the application evolves after testing. Engineering teams can use continuous pentesting to complement periodic assessments. Borg's Whitebox Pentest (Mjolnir) uses source code context to map the attack surface, generate test cases, and test live applications and APIs. It is designed to identify issues such as broken tenant isolation and business logic vulnerabilities, with findings that help engineers investigate and reproduce potential security issues. Its coverage depends on the application context, accessible attack surface, and testing performed.
Conclusion
Tenant boundaries do not hold because of good intentions; they hold because your engineering team designs, tests, and maintains the controls that enforce them. Relying solely on pattern-matching scanners or periodic penetration tests can leave application-specific authorisation logic insufficiently tested as the codebase evolves. Continuous security testing, combined with sound design, code review, and appropriate manual assessment, helps identify weaknesses throughout the development lifecycle.
Build your cross-tenant test matrix, integrate boundary checks directly into your CI pipeline, and use automated retesting to help verify fixes for sensitive pull requests. When you need continuous offensive security testing to help identify exploitable business logic flaws and broken tenant isolation in real applications, request a walkthrough of Borg at borghq.io.
Frequently asked questions
- What is the difference between tenant isolation testing and RBAC testing?
- Role-based access control (RBAC) testing verifies whether users can perform actions permitted by their assigned roles, such as checking whether a Viewer can perform an Admin-only action. Tenant isolation testing verifies that users cannot access or manipulate resources belonging to another tenant unless explicitly authorised. The two types of testing complement each other: a user may have permission to perform an action within their own tenant but not on another tenant’s resources.
- Can static analysis (SAST) tools detect broken tenant isolation?
- SAST tools can identify some potential tenant-isolation issues, but they may miss authorisation flaws that depend on runtime context, data ownership, or application-specific business logic. An unscoped database query can be syntactically valid while still exposing another tenant’s data. That is why SAST should be combined with cross-tenant integration tests and, where appropriate, offensive testing against a running application.
- How often should an engineering team run tenant isolation tests?
- Run automated cross-tenant regression tests in CI on pull requests that change authentication, authorisation, routing, database schemas, or data-access logic. Run broader isolation test matrices when the scope of a change warrants them, and use periodic or continuous offensive testing to look for issues that regression tests may miss. Test against an appropriately scoped staging or production environment, with safeguards in place to avoid disrupting users or data.
- How do you test tenant isolation when using Row-Level Security (RLS)?
- To test RLS, run integration tests against a real PostgreSQL instance using an application role that is subject to the relevant row-security policies and does not bypass them. Within a transaction, set the tenant context used by your policies (for example, with SET LOCAL app.current_tenant_id if your application is configured to use it). Verify that queries without manual tenant filters can access only the active tenant's permitted rows, and test relevant insert, update, and delete operations to confirm that the policies enforce the intended boundaries.



