Multi-tenant data leak
Theory
Why This Matters
Multi-tenancy data isolation failures can expose every customer of a SaaS platform simultaneously. In 2019, a researcher found that a major HR SaaS provider served employee records from one company to a user authenticated as a different company's employee — a textbook cross-tenant IDOR caused by a missing tenant_id filter in a database query. In 2023, a misconfigured shared-cache layer in a large cloud productivity suite caused users to receive cached responses containing other tenants' documents. OWASP API Security Top 10 API1:2023 (Broken Object Level Authorization) and API5:2023 (Broken Function Level Authorization) both apply: the failure is simultaneously an object-level authorization gap (missing tenant filter) and a function-level gap (tenant context not enforced in middleware). The blast radius in multi-tenancy failures is uniquely severe — a single exploited endpoint may expose data from thousands of unrelated organizations.
Core Concept
Multi-tenancy is an architecture in which a single application instance and often a single database serve multiple independent customers (tenants). Each tenant's data must be logically or physically isolated from every other tenant's data. Three common isolation models exist, each with distinct security profiles.
Shared-database, shared-schema (most common in SaaS): all tenants' rows coexist in the same tables, distinguished only by a tenant_id foreign key column. Isolation depends entirely on every query including a WHERE tenant_id = :current_tenant clause. One missing clause exposes all tenants' data.
Shared-database, schema-per-tenant: each tenant has its own set of tables (schema namespace) within a shared database engine. Isolation depends on correct schema-switching at connection time. A bug that fails to switch schemas, or that falls back to a default schema, leaks across tenants.
Database-per-tenant: strongest isolation; each tenant has a dedicated database. Cross-tenant leakage requires a connection-routing error or credential confusion.
The violated invariant in shared-schema deployments is tenant context propagation: the tenant identity (derived from the authenticated user's JWT claims, session, or API key) must be threaded through every database query as an additional filter. When a developer writes SELECT * FROM orders WHERE order_id = ? instead of SELECT * FROM orders WHERE order_id = ? AND tenant_id = ?, the query returns results for any tenant whose order happens to match the ID.
JWT sub/tenant claim tampering is an escalation vector: if the application derives the tenant context from a JWT payload field (tenant_id, org_id) that is signed but whose value is trusted without additional server-side validation (e.g., the JWT secret is weak or the claim is in an unsigned portion), an attacker can substitute another tenant's identifier.
Technical Deep-Dive
-- Attacker is authenticated as a user of Tenant A (tenant_id = 100)
-- Their JWT contains: {"sub":"user_77","tenant_id":100,"role":"user"}
GET /api/projects/2045 HTTP/1.1
Host: app.saas-victim.com
Authorization: Bearer <Tenant_A_JWT>
HTTP/1.1 200 OK
{"id":2045,"tenant_id":101,"name":"Tenant B Confidential Project","data":"..."}
-- Cross-tenant leak: tenant_id in response (101) != attacker's tenant (100)
-- Vulnerable query (missing tenant_id filter)
SELECT * FROM projects WHERE id = :project_id;
-- Secure query (tenant_id filter enforced)
SELECT * FROM projects
WHERE id = :project_id
AND tenant_id = :current_tenant_id; -- current_tenant_id from verified JWT claim
# Testing cross-tenant isolation — two-tenant methodology
import requests
# Tenant A credentials
headers_a = {"Authorization": "Bearer <tenant_A_jwt>"}
# Tenant B credentials
headers_b = {"Authorization": "Bearer <tenant_B_jwt>"}
# Get a list of Tenant B's project IDs (as Tenant B)
r = requests.get("https://app.saas-victim.com/api/projects", headers=headers_b)
tenant_b_ids = [p["id"] for p in r.json()["projects"]]
# Attempt to access Tenant B projects as Tenant A
for pid in tenant_b_ids:
r = requests.get(f"https://app.saas-victim.com/api/projects/{pid}",
headers=headers_a)
if r.status_code == 200:
data = r.json()
if data.get("tenant_id") != 100: # 100 = Tenant A's ID
print(f"CROSS-TENANT LEAK: project {pid} (tenant {data['tenant_id']})")
Security Assessment Methodology
- Register two tenants in the target SaaS application. Obtain API credentials (JWT or API key) for each.
- Enumerate Tenant B's resource IDs while authenticated as Tenant B. Record IDs for all object types (projects, users, reports, invoices, etc.).
- Probe cross-tenant access — authenticated as Tenant A, request every Tenant B resource ID. A 200 response with Tenant B's data confirms cross-tenant isolation failure.
- Test JWT claim manipulation — decode the JWT (Base64url), note the
tenant_idororg_idclaim, and attempt to construct a modified JWT with Tenant B's tenant ID. If the JWT secret is weak, use hashcat or jwt-cracker to recover it; if the algorithm isnone, try the none-algorithm bypass. - Test API key cross-tenant reuse — if the application issues API keys, verify that an API key issued to Tenant A cannot be used to access Tenant B's endpoints by substituting Tenant B's resource IDs.
- Check shared cache behavior — if the application uses a shared cache (Redis, Memcached, CDN), probe whether cache keys include the tenant identifier or only the resource ID. A cache key of
project:2045is shared across tenants;project:100:2045is not. - Review error messages — verbose error responses may reference other tenants' data in stack traces or ORM error messages.
Defensive Countermeasure — Enforce tenant context at the ORM/repository layer using a global query scope (e.g., a Django Manager with a default
.filter(tenant=current_tenant), or a Railsdefault_scope), so tenant isolation cannot be omitted on individual queries. Validate tenant identity server-side from the authentication token — never trust atenant_idfield that the client can modify. Use row-level security (RLS) in PostgreSQL as a defence-in-depth layer that enforces tenant filters even if application-level checks are bypassed.
Common Assessment Errors
- Testing only your own tenant's objects — the test requires resources from a different tenant; create a second test account in a separate tenant registration, not just a second user within the same tenant.
- Assuming API key isolation — API keys scoped to a tenant may not be validated against the tenant context of each requested resource; always test cross-tenant access with a different tenant's resource IDs.
- Missing indirect tenant leakage — search, filter, and list endpoints that accept query parameters may return results from other tenants when the tenant filter is absent from the WHERE clause even if individual-resource GET endpoints are protected.
- Not checking shared cache — a cache miss/hit timing difference (or a 304 Not Modified response) can reveal that another tenant's cached response was served; instrument response times and cache-control headers.
- Overlooking subdomain-based tenancy — some SaaS platforms use
tenant-a.saas.comsubdomains; the tenant is often derived from theHostheader, which can be manipulated viaX-Forwarded-Hostif the application trusts proxy headers. - Stopping at the first 403 — one endpoint being protected does not mean all are; systematically test every object type and every HTTP verb.
NICE Framework Alignment
| Code | Knowledge/Skill/Task Statement | How This Card Develops It |
|---|---|---|
| K0007 | Knowledge of authentication, authorization, and access control methods | Explains tenant-context propagation as an access-control requirement and the isolation models that enforce it |
| K0065 | Knowledge of policy-based access control | Covers row-level security and global query scopes as policy enforcement mechanisms for multi-tenant isolation |
| K0070 | Knowledge of common application vulnerabilities | Identifies missing tenant_id filters and JWT claim misuse as concrete vulnerability patterns in SaaS architectures |
| S0001 | Skill in conducting vulnerability scans and recognizing vulnerabilities in security systems | Practises two-tenant cross-probe testing methodology and JWT claim manipulation |
| T0028 | Task: Identify systemic security issues based on vulnerability and configuration data | Develops ability to recognize missing tenant context propagation as a systemic isolation failure across an entire SaaS platform |
Further Reading
- OWASP API Security Top 10 — API1:2023 Broken Object Level Authorization — OWASP Foundation
- Multi-Tenant SaaS Security: Isolation Models and Common Pitfalls — AWS Security Blog
- Hacking APIs: Breaking Web Application Programming Interfaces — Corey Ball, No Starch Press (2022)
- PostgreSQL Row Level Security Documentation — PostgreSQL Global Development Group
Challenge Lab
Reinforce your learning with a hands-on generated challenge based on this card's competency.