Authorization Testing Across Tenant Boundaries
In a multi-tenant product, the worst bug is not a crash — it is one customer seeing another customer's data. These failures are usually broken object-level authorization: an endpoint that checks that a user is logged in but not whether this object belongs to them. Changing an ID in a URL, replaying a request against a different account, or triggering an export for someone else's workspace is enough.
Authorization bugs are hard to catch with ordinary tests because the happy path works perfectly. Every test signs in as the owner of the data it touches, so the missing check never matters. Catching them requires testing deliberately from the wrong side of the boundary: a second tenant, a lower role, a direct object reference, a background job — systematically, for every resource and action.
TL;DR
- Build an explicit matrix of roles × resources × actions with the expected result for each, and generate tests from it.
- Every test environment needs at least two tenants with similar data, so cross-tenant access can be attempted.
- Test every access path, not just REST detail endpoints: lists, search, GraphQL, file downloads, exports, webhooks, WebSockets, background jobs, and caches.
- Cross-tenant requests should behave as if the resource does not exist — usually 404, with no data in counts or error messages.
- Watch for mass assignment: clients must never be able to set
tenant_id,owner_id, orrolein a request body. - Turn every authorization bug found in production or a pen test into a permanent security regression test.
Quick Example
A parameterized test generated from an authorization matrix, run against two tenants.
Adding a row to the matrix adds a test; adding an endpoint without a row should fail a separate check that every route is covered.
Core Concepts
The Vulnerability Classes
BOLA is the first item in the OWASP API Security Top 10 (2023) because it is common, easy to exploit, and invisible to most automated scanners.
The Authorization Matrix
The matrix is the specification of who may do what. Write it as data — in a spreadsheet, YAML, or test code — with one row per role, resource type, and action, and an expected result. It doubles as documentation for product and security reviews and makes gaps visible: an endpoint missing from the matrix is untested by definition.
Two-Tenant Fixtures
Seed test environments with at least two tenants that mirror each other: same roles, same resource types, similar names. Tests sign in as tenant A and attempt every operation on tenant B's objects. Using realistic, similar data also catches bugs where queries accidentally match by name or other non-unique fields.
Status Codes and Leakage
Cross-tenant access should be indistinguishable from accessing a non-existent ID: the same status code (typically 404), the same error body, and similar timing. Within a tenant, 403 is appropriate when the user may know the resource exists. Check bodies, headers, and side channels — totals in paginated lists, autocomplete suggestions, and validation messages — not just status codes.
Paths That Are Easy to Miss
Testing Approaches
Integration Tests From the Matrix
Matrix-driven integration tests, as in the quick example, are the backbone. Run them in CI on every change, against a real database with two seeded tenants.
Route Coverage Checks
Enumerate all routes from the framework's router or OpenAPI specification and fail the build if any route lacks a matrix entry. This turns "someone forgot to add a test" into a red build. See OpenAPI and API Contract Generation.
Dual-Session Dynamic Testing
Proxy tools can replay every request a high-privilege user makes using a low-privilege or other-tenant session and flag responses that succeed. This finds gaps the matrix authors did not think of, particularly in large or legacy applications.
Defense in Depth at the Data Layer
Application checks can be backed by database enforcement — row-level security policies keyed on the current tenant, or separate schemas or databases per tenant. Tests should verify both layers: that the application denies access and that the database would have refused even if the application had not.
Best Practices
Derive Tenant From the Session, Never the Request
The tenant and user identity come from the authenticated session or token. Request bodies, query strings, and headers that claim a tenant are ignored or validated against the session.
Scope Queries by Default
Use repository methods or query builders that require a tenant parameter, so unscoped queries are impossible to write by accident. Code review should treat any unscoped query on tenant data as a defect.
Use Unguessable Identifiers — but Do Not Rely on Them
Random IDs (UUIDs, ULIDs) make enumeration harder, but IDs leak through URLs, logs, and shared links. Authorization must hold even when an attacker knows a valid ID.
Run Synthetic Cross-Tenant Probes in Production
A canary job that signs in as a synthetic tenant and attempts to access another synthetic tenant's objects detects regressions introduced by configuration changes, caching layers, or new services.
Common Mistakes
Testing Only as the Owner
Trusting a Tenant ID From the Client
Forgetting Asynchronous Paths
FAQ
What is BOLA or IDOR?
Broken object-level authorization (also called an insecure direct object reference) happens when an application accepts an object identifier from the client and returns or modifies that object without checking that the caller is allowed to access it. Changing an ID in a URL or request body is often enough to read another user's data.
Should cross-tenant requests return 403 or 404?
Usually 404, with the same body as for a non-existent object, so attackers cannot confirm which IDs exist in other tenants. Use 403 within a tenant when the user may know the resource exists but lacks permission for the action.
How do we test authorization at scale?
Generate tests from a role-resource-action matrix, seed two or more tenants in every test environment, enforce that every route has matrix coverage, and supplement with dual-session dynamic testing. Turn every discovered bug into a regression test so the matrix grows with experience.
Does database row-level security remove the need for these tests?
No — it adds a valuable second layer. Application logic still decides roles, actions, and field-level access, and row-level security policies can themselves be misconfigured. Test both: the application should deny the request, and the database should refuse the query.
How do we test background jobs and exports?
Trigger them as a user in one tenant with filters or IDs that reference another tenant, then inspect the generated output, notifications, and stored files. Jobs should record the requesting tenant and user and scope every query accordingly.
Related Topics
- Identity & Access Management — Identity and access controls overall
- Authorization Models: RBAC, ABAC, and ReBAC — The rules these tests verify
- Multi-Tenant Application Design — Isolation strategies for tenant data
- Broken Access Control — The vulnerability class in depth
- Security Regression Test Suites — Keeping fixed authorization bugs fixed
- Row-Level Security — Database-enforced tenant isolation
- API Testing — Testing APIs more broadly