Multi-tenant authorization testing: prove the boundary, then retest it

For a small B2B SaaS team shipping workspaces or permissions, multi-tenant authorization testing needs more than a tenant ID. Build a matrix of actors, objects, roles, and operations; prove both intended access and denied access; then repeat the checks against the deployed fix.

Read as MarkdownSubscribe via RSS
  1. Verified membership
  2. Object and action
  3. Allowed or denied
Tenant selection, object ownership, and operation permission are separate inputs to one server-enforced decision.

Use two fictional workspaces, Atlas and Birch, and a private report belonging to Atlas. This is a synthetic teaching scenario, not a discovered vulnerability or a completed assessment. A member, viewer, and administrator have different permissions. An administrator in Birch has no special authority over Atlas just because both accounts use the same role name.

Before opening a testing tool, write the policy in ordinary language. For this example: Atlas members may read their own private reports; Atlas administrators may read reports within Atlas; viewers may not read these private reports. No requester from Birch may read the Atlas report. This is a chosen example policy, not a universal rule for SaaS products.

Tenant ID, object ownership, and role answer different questions

A tenant ID identifies the workspace being selected. Object ownership identifies the relationship between a resource and its tenant, creator, assignee, or sharing policy. A role defines allowed operations within its scope. None of those facts alone proves that the current user may perform the requested operation on this object.

Input What it tells you What it doesn’t establish
Selected tenant Which workspace the request concerns That the requester currently belongs to it
Object ID Which report or file is requested That it belongs to the selected tenant or may be accessed
Ownership relationship Which principal or tenant controls the resource That every owner-related operation is permitted
Role A permission class within a scope Cross-tenant privileges or access to every object

OWASP’s Multi-Tenant Application Security Cheat Sheet, section 1 says client-supplied tenant identifiers are selectors only. Bind the selected tenant to a server-verified identity and current membership or explicit service authorization. A hidden field, URL segment, or header cannot grant membership.

The backend must also establish that the report is accessible under that tenant’s policy and that the action is permitted. OWASP API1:2023, Broken Object Level Authorization requires checks for the requested action on the requested object. It explicitly says comparing a session user ID with an object identifier is insufficient for the general problem.

An unpredictable UUID can reduce enumeration. It isn’t authorization. The test doesn’t need to guess identifiers: use known objects created in the authorized synthetic fixtures. That produces a clearer comparison and avoids involving unrelated customer data.

Write the matrix before collecting requests

Build rows from the business rule, not just the routes you happen to notice. Start with actor, tenant, role, resource relationship, and operation. Include any documented sharing or platform-support exception as a separate explicit path.

WSTG 4.2, WSTG-ATHZ-04 recommends at least two users, often more, with different objects and relevant privileges. WSTG-ATHZ-02 covers horizontal access between identities and vertical access between privilege levels. For a multi-tenant application, include both same-tenant role differences and cross-tenant relationships.

For the private-report read policy, use these controls:

Requester and relationship Expected decision Purpose
Atlas member, own report Allow Positive control for ordinary intended access
Atlas member, another member’s report Deny Same-tenant object restriction
Atlas administrator, another member’s report Allow Positive control for the documented admin exception
Atlas viewer, either owner Deny Role restriction
Birch administrator, Atlas report Deny Tenant boundary despite elevated local role
Birch member, Atlas report Deny Tenant boundary for ordinary user

Use separate sessions and clearly labeled fixtures. Confirm that the protected object exists with an authorized control. If every operation fails because authentication expired, nothing about tenant isolation has been established. Preserve that run as blocked and repair access setup before evaluating the policy.

Read, write, and business actions need separate controls

A passed read check cannot establish a passed update, deletion, export, or invitation check. Those operations can use different handlers, policies, and background jobs. Don’t compress them into a single “report access” result.

OWASP ASVS 5.0.0 V8 separates function permission (v5.0.0-8.2.1), object permission (v5.0.0-8.2.2), and field permission (v5.0.0-8.2.3). Requirement v5.0.0-8.4.1 addresses cross-tenant controls. Requirements v5.0.0-8.1.1 and v5.0.0-8.1.2 call for documented rules; mapping a test to these IDs does not establish full ASVS verification.

Extend the read matrix with application-specific rows:

  • Write: an authorized member changes an allowed report title; the same member cannot change protected ownership, tenant association, or restricted fields.
  • Action: an allowed administrator starts an export; a viewer cannot start it even if they can see some report metadata.
  • Delivery: the export contains only permitted objects and fields; a different tenant cannot fetch its result.
  • Lifecycle: removing a membership stops the access required by policy while another authorized member retains functionality.
  • Bulk: each selected object is authorized; an allowed item must not smuggle a forbidden one into the same request.

These distinguish API3:2023, property-level authorization from API5:2023, function-level authorization. A user may be allowed to edit an object but forbidden to set its billing status. A user may also be forbidden to invoke an administrative function at all.

For read denials, check that no protected content, fields, or download capability was returned. For writes, compare state before and after. For actions, inspect queues, notifications, exports, and downstream changes through authorized evidence access. A denial message isn’t sufficient if the operation already happened.

Carry the boundary into storage, caches, and workers

Tenant isolation must survive each path that handles tenant-owned data. The visible HTTP route is only one point of enforcement. OWASP’s multi-tenant guidance, sections 2 and 4–6 covers database access, protected cached values, asynchronous work, and storage signing.

1. Requester
Verified identity and selected tenant membership.
2. Export request
Permitted action, report set, and returned fields.
3. Worker
Trusted job context; permission checked for delayed work.
4. Download
Authorize the exact result before serving or signing it.
Synthetic export flow. A check at creation time doesn't automatically protect later worker execution or result delivery.

If PostgreSQL row-level security is part of the design, test through the actual request role. PostgreSQL 18 documentation, section 5.9 states that superusers and BYPASSRLS roles always bypass row security; table owners normally bypass it unless forced. FORCE ROW LEVEL SECURITY doesn’t constrain the first two cases. A policy file alone can’t prove the deployed connection uses a constrained role.

Exercise reused connections with both tenants. Verify that tenant context is re-established and cannot leak from the previous request. Inventory tenant-owned tables so new ones cannot silently escape the policy. These are architecture-specific checks; an application using separate databases needs evidence for its credential and routing boundaries instead.

A tenant-scoped cache key prevents one class of mixing but doesn’t grant permission to read the cached value. Authorize before returning protected cached content. Likewise, a tenant-prefixed storage key isn’t an access policy. Check permission for the exact object and operation before generating a signed URL, and document how its lifetime relates to membership removal.

For delayed exports, authenticate the producer path, preserve trusted context, and re-establish authorization at the consumer. Test removal between request and execution according to the declared policy. Include permitted service identities without assuming every internal worker has universal access.

Use the local lab to distinguish agreement from security

The authorization lab attached to this article is a local simulation. It models report-104, which belongs to Atlas, and compares the chosen read policy with a deliberately insecure ID-only lookup. Its implementation declares the insecure result allowed for every selection. It doesn’t query your app, send a scan, or enforce a production boundary.

Start with an Atlas member reading their own report: both modeled decisions allow. That agreement doesn’t make the lookup secure. Change only the owner to another member: the policy denies while the insecure lookup still allows. Next choose an Atlas administrator: the policy allows either owner, because the administrator exception is scoped to Atlas.

Finally choose Birch with the administrator role. The policy denies, and the insecure lookup allows. This separates tenant membership from role privilege. An Atlas viewer is also denied by the policy, even for the owner setting labeled “Requester.” Ownership doesn’t override this scenario’s private-report role rule.

Those are modeled policy outcomes, not observations from a real application’s access controls. Local browser tests exercise all 12 tenant/role/owner combinations and check that interaction sends no network requests. The lab covers one read decision. It doesn’t cover session validation, writes, export actions, sharing exceptions, database policies, or concurrency. Use it to understand which policy input changed, then build the broader matrix for your actual product.

Record the failure precisely and retest after deployment

An evidence record should let another authorized engineer distinguish a missing check from a fixture mistake. Use a stable finding reference and capture:

Field What to retain privately
Target Environment, deployed revision, policy/configuration version, timestamp
Actor Synthetic user, authenticated session reference, tenant, role, current membership
Resource Object reference, verified tenant association, owner or sharing relationship
Operation Read, specific field update, delete, export, download, or other named action
Controls Intended successful case and closely matched forbidden case
Outcome Sanitized sensitive output, before/after state, or downstream event evidence
Limits Untested paths, inaccessible state, timeouts, and unresolved interpretation

“The handler may omit a tenant predicate” is a hypothesis. A reproducible prohibited read or mutation confirms a narrower runtime effect. Don’t infer compromise of every tenant from one synthetic object. Investigate breadth within scope and preserve uncertainty where evidence is missing. WSTG 4.2’s Reporting chapter, section 3 recommends replication, impact, remediation detail, and masked sensitive data.

Fix the enforcing boundary, then retest against the deployed revision. Repeat the original case and relevant variants: another object, sibling operation, cached response, or delayed job. Positive controls must still work. Record verified closure for the specific finding, leaving excluded workflows visible. A code merge or reviewer approval is not the retest result; the coding-agent security guide explains that distinction for generated repairs.

Authorization release acceptance checklist

  • Every included operation has a written policy, including sharing and administrative exceptions.
  • Synthetic fixtures cover two tenants, relevant roles, own and other objects.
  • Authorized reads, writes, and actions succeed as separate positive controls.
  • Forbidden reads disclose nothing protected; forbidden writes and actions produce no prohibited effects.
  • Removal, cache, storage signing, workers, and effective database roles are checked where relevant.
  • Failures include reproducible sanitized evidence and a named fix owner.
  • Deployed retests preserve legitimate behavior and identify the tested revision.
  • Blocked and untested matrix cells remain visible rather than counted as passes.

Decide what the evidence supports before rollout

The matrix can support a bounded release decision, not a claim that the entire application is secure. Our website pentesting guide explains scope and report acceptance; the statistics reference and AI pentesting leaderboard reference explain why external numbers can’t fill untested cells.

Pensec, which we build, currently accepts early-access readiness requests. Submitting the form does not start automated testing. Ownership, authorized targets, accounts, actual delivery capacity, and retest terms need separate confirmation. You can prepare this matrix internally and use it to brief an independent assessor without exposing real customer data.

Sources and review notes

Primary guidance retrieved 2026-10-07. The scenario, matrix, and acceptance checklist are editorial synthesis, not empirical findings. Revisit them when roles, sharing rules, storage paths, or job behavior change.

Local simulation

Authorization matrix lab

This fictional report belongs to tenant Atlas. Choose a requester to compare an explicit access policy with a deliberately insecure ID-only lookup. Everything runs in this browser; no requests or scans are sent.

Expected policy decision

Allow

Allowed: a member can read their own report.

Insecure ID-only lookup

Allow

Returns report-104 without checking tenant, role, or owner.

Decisions agree for this selection. That does not make the lookup secure.

View the complete simulated matrix
Policy: same tenant, and either administrator or member reading their own report. Viewers are denied.
TenantRoleOwnerExpectedInsecure
atlasviewerselfDenyAllow
atlasviewerotherDenyAllow
atlasmemberselfAllowAllow
atlasmemberotherDenyAllow
atlasadminselfAllowAllow
atlasadminotherAllowAllow
birchviewerselfDenyAllow
birchviewerotherDenyAllow
birchmemberselfDenyAllow
birchmemberotherDenyAllow
birchadminselfDenyAllow
birchadminotherDenyAllow

Synthetic teaching simulation, not production authorization code. Real systems need server-side checks and tests against their actual policy.

About Gabe

@bucabay

Gabe writes about website pentesting, security research, and coding-agent security at Pensec.

More from Gabe