Chapter 2 · Practice 1 of 4
Scope every lookup to what the caller may access
Why it matters
An insecure direct object reference (IDOR), called broken object level authorization (BOLA) in the OWASP API Security Top 10, is when GET /invoices/1043 returns invoice 1043 to anyone who asks. It is the most common API flaw: BOLA is number one on the API Security Top 10 2023.
ASVS 5.0 requirement 8.2.2 asks that access to specific data items is restricted to consumers explicitly allowed to access them. OWASP's IDOR Prevention Cheat Sheet shows the cleanest way: put the ownership condition in the query instead of fetching first and remembering to compare later.
| Pattern | Example | Risk |
|---|---|---|
| Global lookup | Invoice.find(id) | returns anyone's invoice |
| Fetch then compare | find(id), then if (inv.ownerId !== user.id) | safe only if nobody forgets the if |
| Scoped query | find({ id, accountId: user.accountId }) | other accounts' rows are never loaded |
Take the user and account IDs from the session, never from the URL or body. And check every action, not just reads: create (into which project?), update, delete, export, share and restore all need the same scope.
Example
// Unsafe: any signed-in user can read any invoice by changing the ID
const invoice = await db.invoice.findUnique({ where: { id } });
// Safe: the ownership rule is part of the query, IDs come from the session
const invoice = await db.invoice.findFirst({
where: { id, accountId: session.accountId },
});
if (!invoice) throw new NotFoundError(); // same answer for "missing" and "not yours"