Finding IDOR in multi-tenant SaaS
TrekShield Security Team · January 14, 2026 · 8 min read
Broken object-level authorization — usually called IDOR (Insecure Direct Object Reference) — is consistently one of the highest-impact and most-missed classes of web vulnerability. It sits at the top of the OWASP API Security Top 10 for a reason: it is trivial to introduce, invisible to most automated tooling, and in a multi-tenant product it can mean one customer reading another customer’s data.
What IDOR actually is
An IDOR exists when an application uses a client-supplied identifier to fetch an object, but fails to verify that the currently authenticated user is allowed to access that specific object. Authentication answers “who are you?”. Authorization answers “are you allowed to touch this?”. IDOR is an authorization failure: the user is logged in, but the server never checks ownership.
Consider a typical invoice endpoint:
GET /api/v2/invoices/10432
Authorization: Bearer <valid session for user A>
If the server loads invoice 10432 and returns it without confirming that it belongs to user A’s tenant, then changing the number to 10433 may return a different tenant’s invoice. Same session, different object, no check.
Why multi-tenant SaaS is especially exposed
In a multi-tenant architecture, every request carries an implicit boundary: the tenant. The safest designs scope every query to the tenant derived from the session — never from the request body or URL. Problems creep in when:
- The tenant identifier is taken from a client-controlled field (a header, query param, or JSON body) instead of the session.
- A newer endpoint (often a bulk export, webhook, or “v2” API) reuses an object lookup but forgets the tenant filter present elsewhere.
- An object is reachable by two paths — one guarded, one not — and only the obvious path was reviewed.
- Identifiers are sequential integers, making neighboring objects easy to enumerate.
The most damaging IDORs we see are rarely on the primary flow. They hide in the endpoints nobody demos: exports, admin-adjacent tooling, and the second version of an API.
Why scanners miss it
Automated scanners are pattern matchers. They excel at reflected XSS, known-CVE fingerprinting, and misconfigurations. But IDOR has no signature — a 200 OK returning another tenant’s invoice looks identical to a 200 OK returning your own. Detecting it requires understanding what the object is, who should own it, and whether the response proves a boundary was crossed. That is a semantic judgment, not a string match — which is exactly why it needs a human.
How we test for it by hand
Our approach is systematic, and it always uses at least two accounts so we can prove a real cross-tenant boundary rather than guess at one.
- Map objects and identifiers. Catalogue every endpoint that accepts an ID, and note the ID format (sequential, UUID, slug) and where the tenant is derived from.
- Provision two isolated tenants. Account A and Account B, each with their own records, created through the product’s normal flows.
- Swap references. As Account A, replay requests substituting B’s object identifiers — in the path, query, body, and any nested references.
- Test every verb. Read is only the start.
PUT,PATCH, andDELETEagainst another tenant’s object are frequently worse than read. - Follow the quiet paths. Bulk exports, report generation, file downloads, and webhook callbacks often bypass the checks on the main API.
Proving impact, not probability
Finding a suspicious response is not the finding. The finding is demonstrable impact. For every IDOR we report, we attach:
- The exact request/response pair showing Account A retrieving Account B’s data.
- Confirmation that the two accounts are genuinely separate tenants.
- A clear statement of blast radius — one record, or every record via enumeration.
- A CVSS vector with the business context that drives its severity.
If we can’t reproduce it, we don’t report it. That discipline is what makes a report something your engineers act on instead of argue with.
How to fix and prevent it
- Derive tenancy from the session, never the request. The authenticated context is the source of truth for which tenant a request belongs to.
- Enforce ownership at the data layer. Scope every query by tenant (and where relevant, by user) so an unauthorized object simply cannot be loaded.
- Centralize authorization. A single, well-tested policy layer beats per-endpoint checks that drift over time.
- Prefer unguessable identifiers. UUIDs raise the bar on enumeration — but they are defense in depth, not a substitute for an ownership check.
- Add regression tests. For each object type, assert that Account A cannot read or modify Account B’s objects. Run it in CI.
The takeaway
IDOR is a design-level authorization problem wearing the disguise of a simple bug. It won’t show up on a scan, and it won’t announce itself in your logs. It takes a tester with two accounts, a methodical eye, and the discipline to prove impact before calling it a finding — which is exactly how we test every application we touch.
Want proof your tenants are actually isolated?
A short scoping call, no obligation.