IDOR & Broken Access Control
Change one number in a URL and read someone else's invoice. That is IDOR - and the broader class, broken access control, is the most common serious vulnerability on the web. It is also one of the most preventable.
Broken Access Control is the failure to enforce what an authenticated user is allowed to do. Its most common form is IDOR - Insecure Direct Object Reference - where the app exposes a reference to an object (an ID) and then acts on it without checking the requester actually owns it. In the API world the same flaw is called BOLA (Broken Object Level Authorization), and it is the number-one API security risk.
IDOR vs BOLA
They are the same bug in different clothes. IDOR is the classic web term; BOLA is the API-era name from the OWASP API Top 10. Both describe: the system authenticated you, but it never checked whether this specific object is yours. Authentication answers "who are you?"; authorization answers "are you allowed to touch this?" IDOR/BOLA is a missing answer to the second question.
A worked example
An invoice endpoint returns a document by ID:
GET /api/invoices/1043 -> your invoice (200 OK)The ID is sequential and the server only checks that you are logged in - not that invoice 1043 belongs to you. So you try the neighbours:
GET /api/invoices/1042 -> someone else's invoice (200 OK)
GET /api/invoices/1041 -> another customer's invoice (200 OK)A short script now walks every ID and downloads the entire customer base's billing data. No exploit, no payload - just a missing ownership check and a for-loop.
Swapping the ID for a random UUID is not a fix - it is obscurity. UUIDs leak through logs, referrers, and other endpoints. The only real fix is an authorization check.
Why it happens
- Trusting the client. The UI only ever shows a user their own IDs, so developers assume the server does not need to re-check - but the API accepts any ID.
- Authorization scattered per-endpoint instead of enforced centrally, so one route inevitably forgets the check.
- Multi-tenancy afterthoughts. An
orgIdis added later, and old queries still fetch by primary key alone.
How to detect it
Create two accounts. As user A, note an object's ID. As user B, request A's object. If B gets it, that is an IDOR. Systematically, test every endpoint that takes an object identifier - view, edit, delete, and export - with a second, unrelated account. It is tedious and extremely high-yield; it is one of the first things we check in a VAPT and a reliable earner in bug bounty.
How to prevent it
- Authorize every object access, server-side. On every request, confirm the current user owns (or is permitted) the specific object - ideally by scoping the query itself.
- Scope the database query, not just an
ifstatement. FetchWHERE id = ? AND owner_id = <current_user>. A non-owned object simply returns "not found," so there is no code path that can forget the check. - Deny by default. New endpoints should require an explicit authorization decision, not inherit access.
- Centralise the logic. A single authorization layer or middleware beats per-route checks that drift out of sync.
- Enforce tenant isolation everywhere - the object must belong to the caller's organisation, not just to some valid user.
// Vulnerable: fetch by id, then act - ownership never checked
const invoice = await db.invoice.find(id);
return invoice;
// Safe: the query itself is scoped to the caller
const invoice = await db.invoice.findFirst({
where: { id, ownerId: currentUser.id } // not yours => not found
});
if (!invoice) return notFound();
return invoice;Key takeaways
- IDOR and BOLA are the same bug: a missing object-level authorization check.
- Random IDs are obscurity, not a fix.
- Scope the query to the owner so there is no unchecked path.
- Deny by default, centralise authorization, and enforce tenant isolation.
Unnbugify provides consent-based VAPT, red teaming, cloud and API security testing, and secure code review for organisations of every size.