Vulnerabilities/Broken Object Level Authorization (BOLA / IDOR)
Broken Object Level Authorization (BOLA / IDOR)
Broken object level authorization, also called insecure direct object reference (IDOR), happens when an API confirms who the caller is and then fetches or changes a record chosen purely by an ID from the request. Change /api/invoices/1001 to /api/invoices/1002 and the server hands over another customer's invoice. It tops the OWASP API Security Top 10 because every endpoint that takes an ID is a candidate, and scanners cannot tell which records a user should see. The fix is the same in every stack: put the ID and the authenticated owner or tenant in the same query, take the owner only from the session, accept writes only through a schema that lists editable fields, and answer 404 for records that belong to someone else. Pick your language below for the exact ORM calls, vulnerable and fixed code, and tests that catch it.
Plain English Explainer (ELI5)
At a theatre cloakroom the attendant checks that you are wearing tonight's wristband, then hands over whichever coat matches the number you say out loud. Your ticket says 12, but you say 57 and walk out with a stranger's coat and the house keys in its pocket. Nobody checked that 57 was your ticket, only that you were a guest. An API with broken object level authorization works the same way: it checks that your login is valid, then loads invoice 1002 because the URL says 1002. The fix is for the attendant to search only the coats hung under your name, so every lookup asks for this number and this owner together.
Core Concepts & Subsystem Terms
- BOLA / IDOR (CWE-639)
- The API lets a client-supplied key (a path segment, query parameter or body field) select which record is read or changed, without checking that the record belongs to the caller.
- Authentication is not authorization
- A valid session, JWT or API key proves identity only. Every handler still has to decide whether this identity may touch this particular record.
- Tenant-scoped query
- Load records with the ID and the caller's organization or owner in the same
WHEREclause, so another tenant's record is simply not found. The owner comes from the authenticated principal, never from the request. - Mass assignment of owner fields (CWE-915)
- Binding a request body straight onto a stored record lets a client set
owner_id,organization_idor the ID itself, moving records between tenants. Accept only a DTO or schema that lists the editable fields. - Unguessable IDs
- UUIDs and random IDs make guessing harder but leak through links, logs, exports and other API responses. They reduce enumeration; they never replace the ownership check.
Step-by-Step Attack Flow
The attacker signs in with a real account
A trial or low-privilege account passes every authentication check the API has.
They swap an ID in a request
They replace their own ID with a neighbour's, or reuse IDs seen in exports, emails or other responses. Two test tenants with sequential IDs illustrate the case.
The handler loads the record by ID alone
A primary-key lookup such as findById, db.get or findUnique returns the row whoever owns it.
Data leaks or changes across tenants
The attacker reads, edits or reassigns other customers' records and can walk the whole ID range. The 2019 First American exposure is a separate incident; its reporting does not establish this exact authenticated API flow.
BOLA / IDOR guides by language
Engineering & System Hardening Checklist
- Add a CI rule (Semgrep or a grep) that flags primary-key lookups such as
findById,db.getandfindUniqueinside request handlers. - Route every read and write of a tenant-owned record through one scoped repository method or helper that takes the caller's organization.
- Take the owner or organization only from the authenticated principal, never from the URL, query string or body.
- Accept writes only through DTOs or schemas that list editable fields and exclude owner and ID columns.
- Return 404, not 403, for records that exist in another tenant.
- Test every route with two tenants that request each other's IDs, and add database row-level security as a backstop.