Hunting IDOR at Scale: From One Object to Every Tenant
Insecure direct object references are still everywhere. Here's the methodology we use to turn a single leaked ID into a full multi-tenant data exposure — and how to shut it down.
Broken object-level authorization tops the OWASP API Security list for a reason: it is trivial to introduce and easy to miss in review. On a recent authorized engagement, a single predictable order ID became the thread that unraveled an entire tenant boundary.
Finding the first reference
We start by mapping every endpoint that accepts an identifier — path params, query strings, and JSON bodies. The interesting ones return objects you own. The vulnerable ones return objects you shouldn't.
httpGET /api/v2/orders/10482 HTTP/1.1 Authorization: Bearer <user-a-token> 200 OK -> belongs to user A
Swap the token for user B and request the same object. If it still returns 200, the server is trusting the identifier instead of the session. From there we fuzz the numeric range and watch the response sizes.
Scaling the finding
- Enumerate sequential IDs to estimate total exposed records.
- Pivot to related endpoints that share the same authorization gap.
- Confirm whether tenant scoping is missing entirely or just on this route.
The fix
Authorization must be enforced at the data layer, scoped to the authenticated principal — never inferred from a client-supplied ID. Centralize the check so a new endpoint can't forget it.
tsconst order = await db.order.findFirst({ where: { id, tenantId: session.tenantId }, }); if (!order) return forbidden();
root@offseccodes:~$ ./engage.sh
Want this tested on your own systems?
We run authorized assessments that find exactly these issues before attackers do.
Start an engagement