Authorization Best Practices

Chapter 2 · Practice 1 of 4

Scope every lookup to what the caller may access

From Object-level authorization: stop IDOR and BOLA

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.

PatternExampleRisk
Global lookupInvoice.find(id)returns anyone's invoice
Fetch then comparefind(id), then if (inv.ownerId !== user.id)safe only if nobody forgets the if
Scoped queryfind({ 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

TypeScript
// 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"