IDOR and broken access control: when users see data that is not theirs

Change an ID in the URL and see someone else's invoice. IDOR is simple, common and serious. Here is how it happens and how to stop it.

A key opening a door that belongs to someone else

IDOR (Insecure Direct Object Reference), also called BOLA (Broken Object Level Authorization) in APIs, is a flaw where the app uses an identifier from the request to fetch an object without checking that the current user may access it.

It belongs to Broken Access Control, which OWASP ranked first in its 2021 Top 10.

A simple example

You open your invoice:

GET /api/invoices/1001

The server checks that you are logged in, and returns invoice 1001. If you change the number to 1002 and get another customer's invoice, that is an IDOR. The app checked authentication (who you are) but not authorization (whether this invoice is yours).

Where it appears

  • IDs in URLs and API paths: /users/42, /orders/9001.
  • IDs in request bodies and query strings: ?account_id=7.
  • File downloads: /download?file=report_12.pdf.
  • Hidden form fields and mobile app API calls.
  • Actions, not just reads: update, delete and "change role" endpoints.

Why it is so common

  • Developers check the user is logged in, but forget a per-object check.
  • Every new endpoint needs the check, and one forgotten endpoint is enough.
  • Automatic scanners cannot tell which objects belong to whom, so it is often found only by testing or review.

The real fix: check ownership on the server, every time

Include the user in the query, so a missing permission means "not found":

SELECT * FROM invoices WHERE id = $1 AND customer_id = $2;   -- $2 is the logged-in user

If the row does not come back, return 404 or 403. Prefer a consistent response so attackers cannot learn which IDs exist.

Good practices

  • Centralise authorization (a policy layer, middleware or database row-level security), so each endpoint does not reinvent it.
  • Deny by default. A new route should be forbidden until it explicitly allows access.
  • Take the user identity from the session or token, never from a request field such as user_id.
  • Check role-based permissions too (admin vs normal), for both reading and writing.
  • Apply the check to every method (GET, PUT, DELETE) and to related objects (an order's items, an invoice's PDF).
  • Write tests that log in as user A and try to read and change user B's objects, and expect failure.

What is not enough

  • Unpredictable IDs (UUIDs) make guessing harder, but they leak through logs, links and emails, so they are only defence in depth.
  • Hiding links in the interface.
  • Client-side checks. Anyone can send requests directly.

Testing legally

Test your own application with two test accounts, or systems where you have written permission (for example a bug bounty program that allows it). Do not access real users' data you are not authorised to see.

Takeaway: authenticate and authorize every request on the server, scope queries to the current user, centralise checks, and test with two accounts.


Web Security series, Part 1 (injection and access control): 4 of 4. Previous: CSRF explained: how it works and how to prevent it.

Keep reading