Multi-Tenant SaaS Isolation Bugs: Cross-Org Data Leaks That Pay
SaaS products sell “your data stays in your workspace.” Hunters break that promise by changing one ID, one header, or one GraphQL argument until another tenant’s objects appear. Isolation failures are IDOR’s richer cousin: same class of mistake, higher business impact.
You need two accounts in two orgs (or a second trial workspace). Without that, you are guessing.
Mental model
Every object should be keyed by tenant_id (or org_id) and authorized against the caller’s membership. Frontends hide other tenants. APIs sometimes do not.
Ask on every request: “Would this succeed if I belonged to org B?” Then prove it.
High-yield surfaces
- REST paths like
/api/projects/{id},/files/{uuid},/invoices/{number} - Bulk endpoints: export, search, “recent items,” analytics rollups
- Invite and member lists that accept
orgIdfrom the client - Shared resources: templates, integrations, webhook logs
- Background jobs that process IDs without re-checking tenant
- Support/impersonation tools that leak across customers when mis-scoped
Search and reporting features are notorious. Engineers filter UI tables carefully and forget the JSON API behind the chart.
Hunting workflow
- Create org A and org B with distinct objects (unique string markers in names/notes).
- Capture org A’s object IDs from normal use.
- Replay those requests with org B’s session cookies/tokens.
- Try swapping only the object ID, then only a tenant header/body field, then both.
- Hit list/search endpoints with filters that reference A’s IDs or strings.
If GraphQL is in play, mutate id / organizationId arguments independently. Batch queries sometimes authorize the first node and return siblings unchecked.
Strong proof vs noise
Strong: org B retrieves org A’s private document, PII, billing artifact, or secret-bearing integration config. Reproducible with two test accounts.
Noise: public marketing assets, docs intended to be global, or IDs that always 404 equally for everyone.
Cross-tenant write (edit/delete) beats read-only for severity — still document reads clearly; many programs pay well for confidential data exposure alone.
Watch background workers next. A synchronous GET may enforce tenant checks while an async “generate report” job loads rows by primary key only. Create the job as org B with org A’s ID in the payload and download the artifact when it finishes.
Common root causes
- Authorize middleware checks “logged in” but not “member of resource’s org”
- Composite keys enforced in SQL for some tables, skipped for legacy ones
- Caching keyed only by object ID
- Elasticsearch/OpenSearch indexes without tenant filters on every query
- “Platform admin” flags reused for customer admins
Report shape
Include both sessions (redact secrets), the exact request that crossed tenants, and the unique marker proving ownership. Recommend server-side tenant binding on every data access, deny-by-default queries, and automated tests that attempt cross-org reads on new endpoints.
Reference OWASP IDOR testing and CWE-639 (authorization bypass through user-controlled key).
Two workspaces and one swapped ID still find a surprising share of SaaS payouts. Automate the happy-path ID swap across new endpoints whenever the product ships a feature module — isolation regressions love fresh tables.