Writing

What is an IDOR Vulnerability? Authentication vs. Authorization & How to Prevent BOLA

If changing /invoices/1042 to /invoices/1043 in the URL exposes another customer's private invoice, your backend suffers from IDOR (Broken Object Level Authorization)—the #1 vulnerability on the OWASP API Security Top 10.

Furkan ÖzayOctober 24, 2026 · 3 min read
czCZAY.DEV // EDITORIALEN
GÜVENLIK

#Güvenlik · 3 MIN READ

FURKAN ÖZAYVOL. 2026 // #C61D

Imagine logging into an e-commerce platform, opening your latest invoice at https://example.com/api/invoices/1042, and curiously changing the URL in your browser bar to 1043.

If the server responds with another customer's full name, home address, and tax ID, that system has a catastrophic IDOR (Insecure Direct Object Reference) vulnerability—classified by OWASP as API1: Broken Object Level Authorization (BOLA), the single most common and dangerous flaw in modern APIs.

· · ·

1. Why Does IDOR Happen? Confusing Authentication with Authorization

Almost every backend developer remembers to add an authentication middleware (requireAuth or verifyJWT) to protected routes. However, authentication and authorization answer two completely different questions:

  • Authentication (401 Unauthorized): "Who are you? Are you logged into a valid account?"
  • Authorization (403 Forbidden): "Do you actually own or have permission to access resource #1043?"

The Vulnerable Code Pattern

typescript
// ❌ VULNERABLE: Checks if the user is logged in, but never checks ownership!
app.get('/api/invoices/:id', requireAuth, async (req, res) => {
  const invoice = await db.invoices.findUnique({
    where: { id: Number(req.params.id) }
  });
  return res.json(invoice);
});

Because the attacker is logged into their own valid account, requireAuth passes! They can then write a 5-line script iterating from 1 to 500,000 and scrape your entire database in minutes.

· · ·

2. The UUID Myth: "I Use UUIDv4, So I'm Safe!"

Warning: Security by Obscurity is NOT Security

Replacing sequential integers (1042) with random UUIDs (f47ac10b-58cc-4372-a567-0e02b2c3d479) makes brute-force guessing harder, but it does not fix the IDOR vulnerability! Object IDs frequently leak in public profiles, webhook payloads, browser history, or GraphQL responses. If ownership is not checked, anyone who obtains the UUID can still read or delete the record.

· · ·

3. The True Fix: Enforcing Ownership at the Database Query Level

Never trust an ID from req.params or req.body in isolation. Always bind the lookup directly to the authenticated user's session/JWT identity (req.user.id):

typescript
// ✅ SECURE: Combines object ID with the authenticated user's ID in the WHERE clause
app.get('/api/invoices/:id', requireAuth, async (req, res) => {
  const invoice = await db.invoices.findFirst({
    where: {
      id: req.params.id,
      userId: req.user.id // <- Strict Object-Level Authorization!
    }
  });
 
  if (!invoice) {
    // Return 404 (or 403) so attackers cannot even probe if the ID exists
    return res.status(404).json({ error: 'Invoice not found' });
  }
 
  return res.json(invoice);
});

If you use PostgreSQL, you can go one step further and enforce Row-Level Security (RLS) at the database engine layer so that a query mathematically cannot return rows belonging to another tenant.

Interactive Roadmap & Tool

Read next