Insecure Direct Object References consistently pay out the biggest bug bounties and appear in the most high-profile breaches. Here's why they're so common and how to find them.
In 2021, a researcher found that changing a single number in a URL let him view private medical records belonging to other patients at a US hospital. In 2023, a bug in a major airline's booking API exposed every passenger's personal details by iterating a booking reference. In 2024, an IDOR in a fintech app's API let authenticated users view any other account's transaction history.
Same vulnerability class. Different numbers. Millions in bug bounty paid out. Still appearing in production applications at every scale.
Insecure Direct Object Reference. When an application uses a user-controlled value — a number, a UUID, a filename — to look up an object in the database or filesystem, and doesn't check whether the requesting user is authorised to see that object.
The simplest example:
GET /api/invoices/1042 Authorization: Bearer eyJhbGci...
You're user A, and 1042is your invoice. The server checks your JWT to confirm you're authenticated. But it doesn't check that invoice 1042 belongs to you. Change it to 1041and you get someone else's invoice.
Authentication says "who are you." Authorisation says "what are you allowed to do." IDOR is a failed authorisation check.
The check is easy to forget. You write the endpoint, you add authentication, and you move on. The authorisation check is a separate line of logic that has to be consciously added for every single endpoint that touches user data.
In large codebases with many contributors over many years, some percentage of endpoints will have that check missing. Teams build fast, audit slowly.
It's not just /api/resource/123. IDOR appears in:
{"order_id": 9999} in a request that fetches or modifies an order/download?file=report_user_1042.pdfCreate two accounts. Call them Account A and Account B. Log in as A, create objects — messages, orders, files, reports. Note the IDs. Log in as B, try to access those IDs. That's the core test loop.
# As Account A — note the ID returned
POST /api/documents
→ {"id": "doc_8821", "name": "My Document"}
# As Account B — try to access it
GET /api/documents/doc_8821
→ Should return 403. If it returns the document, it's IDOR.With Burp Suite: capture requests as Account A, copy the session token for Account B, replay the requests with B's token. Burp Repeater makes this fast.
The Autorize Burp extension automates this: it replays every request you make as Account A automatically using Account B's session and flags any that return the same response.
Every endpoint that retrieves or modifies a resource needs to verify ownership or permission, not just that the user is authenticated.
// Bad — only checks auth
const invoice = await db.invoice.findById(invoiceId);
return invoice;
// Good — checks ownership
const invoice = await db.invoice.findOne({
where: { id: invoiceId, userId: req.user.id }
});
if (!invoice) return 403;
return invoice;At scale: use an authorisation layer that enforces access control policies centrally rather than relying on every developer to remember the check on every endpoint.
Everything in this post has a live lab on hackr.gg. Spin up a vulnerable machine and exploit it yourself — no setup, no VPN, runs in your browser.
Open IDOR & Access Control course →