An insecure direct object reference (IDOR) is an access control bug where the app takes an identifier from the user, such as an invoice ID, and returns that object without checking that the user is allowed to have it. The ID itself is not the problem. The missing ownership check is. CWE calls this CWE-639, and the OWASP API Security Top 10 calls it broken object level authorization (BOLA).
Updated October 2026
Why it matters
Most apps store many customers' records in the same tables. The only thing keeping your invoice away from someone else is the check that ties a record to its owner. When that check is missing, any signed-in user may be able to read or change records that belong to other people. One missing line can expose every record of that type.
IDOR bugs are common in APIs that serve mobile apps and single page apps, because those APIs expose object IDs directly and often get less review than the main web app.
A simple illustration
Acme Corp lets members download their own invoices. The page links to an invoice by its ID. The server looks up the invoice by that ID and returns it. It checks that the user is signed in, but not that the invoice belongs to them.
Vulnerable check:
app.get("/api/invoices/:id", requireLogin, async (req, res) => {
const invoice = await Invoice.findById(req.params.id);
res.json(invoice);
});
Corrected check:
app.get("/api/invoices/:id", requireLogin, async (req, res) => {
const invoice = await Invoice.findOne({
_id: req.params.id,
orgId: req.user.orgId,
});
if (!invoice) return res.status(404).end();
res.json(invoice);
});
The corrected version includes the owner in the query itself, so a record from another organization is never loaded. Returning 404 instead of 403 also avoids confirming that the record exists.
Why automated scanners often miss it
An IDOR response looks like a normal success. The scanner gets a valid invoice back and has no way to know it belongs to someone else. Finding the bug needs at least two accounts, knowledge of which objects each account owns, and a comparison of what each can reach. Scanners that crawl as a single user cannot make that comparison. Tests that check for crashes or error strings will pass.
How to prevent it
- Scope every lookup by owner or tenant in the query, not after the fact.
- Use a shared data access layer that adds the tenant filter automatically.
- Treat every ID from the client as untrusted input, including IDs in headers, bodies and nested JSON.
- Check ownership on reads, updates, deletes and exports, not just on the main view.
- Use random identifiers as defense in depth, never as the only control.
- Add tests that request one user's object as another user and expect a denial.
How to test your own app for it
Only test software you own or have written permission to test.
- Inventory every endpoint that accepts an object ID.
- Confirm every such endpoint enforces object ownership on the server.
- Use two test accounts in two test organizations and confirm neither can reach the other's records.
- Review bulk endpoints, file downloads and background exports, which often skip checks.
- Search the codebase for lookups by ID that lack an owner or tenant filter.
How Wakefield helps
Wakefield's blackbox agents sign in as every role your app has (admin, member, guest), map its pages and APIs, build a model of who should see what, then try to cross user, tenant and role lines. A separate verifier replays each finding with a live exploit and a negative control, so you only see bugs that reproduce. See Blackbox testing.