Get a quote

Field guide · Application security

Application security

IDOR: the flaw your scanner keeps missing.

By Abhimanyu Gupta, Founder & Principal Operator

Broken access control has sat at the top of the OWASP risk list for years, and the Insecure Direct Object Reference is its most common face. It is also the single class of bug automated scanners are worst at finding, because there is nothing broken to detect. The server does exactly what it was asked. It just should not have been. This is a field guide to what an IDOR really is, why tools walk straight past it, how we prove it by hand, and the one fix that actually holds.

At a glance

Attack class
Broken access control, object level (IDOR / BOLA)
Typical impact
CRITICAL  Cross-tenant data exposure, account takeover, mass data theft
OWASP mapping
Web: A01:2021 Broken Access Control. API: API1:2023 Broken Object Level Authorization
Access required
Usually one valid, low-privilege account
Prevalence
Among the most common serious findings in web and API testing
Primary tooling
Burp Suite (Repeater, Autorize, Auth Analyzer), a second test account, patience
Reference
OWASP Web Security Testing Guide, Authorization Testing

What an IDOR actually is

An Insecure Direct Object Reference is what you get when an application exposes a reference to an internal object, a database row, a file, a key, and then trusts the value the client sends without checking that the caller is allowed to reach that specific object. The reference is direct because it maps straight onto something real: /orders/40219 is order number 40219. The reference is insecure because the only thing standing between you and someone else's order is your willingness to type a different number.

The crucial word is authorization, and it is worth separating it cleanly from authentication. Authentication answers "who are you," and most applications do it well. Authorization answers "are you allowed to do this, to this object, right now," and that is the check an IDOR skips. The user is perfectly authenticated. They are simply reaching an object that authentication was never meant to gate.

The one sentence version. Authentication is the lock on the front door. Authorization is the question of which rooms your key opens. An IDOR is a building where every key opens every room, and the only thing keeping guests apart is that nobody told them the other room numbers.

Why scanners miss it

A vulnerability scanner is a pattern matcher. It is very good at syntactic bugs: a quote mark that triggers a SQL error, a script tag that reflects unescaped, a header that leaks a version string. Those have a signature. An IDOR does not. When a scanner requests /orders/40219 and the server returns 200 OK with a valid JSON body, every signal the tool understands says success. It has no model of who owns order 40219, so it cannot know that the response is a breach rather than a feature.

This is the heart of it: an IDOR is a semantic flaw, not a syntactic one. Detecting it requires knowing the intended access-control policy, which customer may see which record, and then noticing where the implementation departs from that policy. No signature encodes your business's ownership rules, so no scanner can flag their violation. That is not a gap a better scanner closes; it is a gap a human with two accounts closes.

The taxonomy: horizontal, vertical, BOLA, BFLA

Access-control failures come in a few distinct shapes, and naming them precisely changes how you test and how you fix. Two axes matter: the direction of the escalation, and whether the missing check is on an object or on a function.

TermWhat is missingClassic example
HorizontalOwnership check between peers at the same privilege levelCustomer A reads customer B's invoice
VerticalPrivilege check between levelsA standard user calls an admin-only endpoint
BOLAObject-level authorization (the API name for IDOR)GET /api/users/{id} returns any user
BFLAFunction-level authorizationPOST /api/admin/promote works for anyone

In API testing the object-level case is so dominant that the OWASP API Security project lists Broken Object Level Authorization as API1, the number-one API risk. IDOR and BOLA are the same bug wearing web and API clothes. The rest of this guide uses IDOR throughout, but everything applies equally to a REST or GraphQL back-end.

Where they hide

The obvious IDOR is a small integer in a URL. The expensive ones are the identifiers people assume are safe:

  • Sequential integers. The textbook case. Enumerable, so one bug becomes a full-database export.
  • UUIDs and GUIDs. A random identifier is harder to guess, but a guess is not required if the value leaks, in a shared link, an email, a referrer header, a previous API response, or a mobile app. Obscurity is not access control. An unguessable reference with no server-side ownership check is still an IDOR; it is just a quieter one.
  • Exports and reports. The main record enforces ownership; the PDF export, the CSV download, or the print view often does not.
  • Mobile and partner back-ends. The API behind the app frequently trusts more than the website, on the theory that only the app calls it. The app is not the only thing that can call it.
  • GraphQL node lookups. A single node(id:) resolver that fetches by global ID can expose every type at once if authorization lives in the UI query rather than the resolver.
  • Indirect references. Filenames, S3 keys, and batch-job IDs are objects too. /tmp/report-8842.pdf is an IDOR waiting to happen.

How we test it by hand

The method is unglamorous and it is why tools cannot replace it. It starts with modeling, not fuzzing.

  • Map the objects and the roles. We enumerate every object the application exposes, orders, invoices, messages, users, files, and every role that can touch them. This is the access-control policy the code is supposed to enforce.
  • Use two accounts. We provision at least two users per role. User A performs a legitimate action; we capture the request. Then we replay it verbatim from user B's session, changing only the object identifier. If B receives A's data, the server never checked ownership.
  • Automate the comparison, not the judgment. Burp extensions like Autorize and Auth Analyzer replay every request through a second session and flag responses that look identical across users. The tool does the mechanical replay; a human decides whether an identical response is a bug, which is exactly the judgment a scanner lacks.
  • Test blind IDOR by side effect. Not every IDOR returns data. A 204 No Content that still cancelled someone else's order, or triggered an email to another user, is an IDOR you confirm by observing the effect, not the response body.

A worked example

Here is the shape of a horizontal IDOR as it appears in a report. We are logged in as user A and ask the API for an order that belongs to user B. Nothing about the request is malformed.

idor, horizontal readhttp
# Authenticated as user A (account 8801). Request an order that is not ours.
GET /api/v2/orders/40219 HTTP/2
Host: shop.example.com
Cookie: session=<user-A-session>

HTTP/2 200 OK
Content-Type: application/json

{"order_id":40219,"owner_id":9315,"email":"b@example.com",
 "total":"1,240.00","card_last4":"4471","address":"..."}
# owner_id 9315 is user B. No 403, no ownership check. Increment the id and
# the whole orders table walks out the door. That is the finding.

The proof is not that the request succeeded. It is that it succeeded for the wrong person. We deliver it with both sessions side by side so there is no argument about intent: the same object, allowed for one user, must be denied for the other, and it was not.

Read is bad, write is worse

Most IDOR discussion stops at reading data. The higher-impact cases mutate it. A PATCH /api/users/9315 that lets you change another user's email, followed by a password reset sent to an address you now control, is a full account takeover assembled from two ordinary-looking requests. The same primitive shows up as changing an order's shipping address, approving your own refund, or flipping a boolean the client was never meant to set. When we find a readable IDOR, we always test the write and delete verbs on the same object, because the severity often lives there.

Chaining raises severity fast. A single-object read is a data-exposure finding. The same missing check on a write verb, chained into password reset or role assignment, is account takeover or privilege escalation. We rate IDORs by what the object lets you do, not by whether the response contained data.

The fix that holds

There is exactly one durable fix, and a set of things that feel like fixes but are not.

The durable fix: enforce authorization on the server, for every object, on every request, at the moment of access. Before returning order 40219, the code must confirm that the current session owns, or is otherwise entitled to, order 40219. Deny by default. Centralize the check so it is not reimplemented, and forgotten, per endpoint.

  • Bind objects to the session server-side. Scope every query by the authenticated principal: fetch "this user's order 40219," never "order 40219" with the owner taken from the request.
  • Centralize the policy. A single authorization layer (a middleware, a policy engine, an ownership helper) that every handler must call beats per-controller checks that drift out of sync.
  • Prefer indirect references where you can. Mapping per-session opaque handles to real IDs limits blast radius, though it is a control in depth, not a substitute for the ownership check.

The things that are not fixes: switching integers to UUIDs (hides the reference, does not authorize it), checking the role but not the object (stops vertical escalation, leaves horizontal wide open), and enforcing access only in the UI (the API is the real surface). Each of these closes a symptom and leaves the flaw.

Detection

Even with the fix in place, you want to see attempts. IDOR abuse has a recognizable signature in your own logs: a single session requesting a wide range of object identifiers it has no legitimate relationship to, a spike in 403 responses once server-side checks are added, or one account reading records across many tenants in a short window. Log authorization decisions, not just authentications, and alert on a principal touching objects far outside its normal set. The fix stops the breach; the telemetry tells you someone tried.

What an IDOR is, and is not

  • It is not authentication bypass. The attacker is usually a valid, logged-in user. That is what makes it so common: it needs only a single ordinary account, not a broken login.
  • It is not always low severity. "Just an IDOR" can mean reading one record or exporting an entire customer base and taking over accounts. Severity is a function of the object and the verb, not the bug class.
  • It is not solved by obscurity. A random identifier reduces guessing, not exposure. If the reference leaks and the server does not check ownership, it is still an IDOR.
  • It is not a scanner finding. If a report of "no broken access control" came from an automated tool alone, it means the tool could not test for it, not that the application is safe.

Key takeaway

An IDOR is a policy question the code forgot to ask: not "is this request valid" but "is this object this caller's to touch." No scanner holds your ownership rules, so no scanner can catch their violation.

The test is two accounts and a swapped identifier. The fix is a server-side authorization check on every object, every request. Everything else is decoration.

References & further reading

  1. OWASP, "A01:2021 Broken Access Control", OWASP Top 10. The category IDOR falls under, and why it ranks first.
  2. OWASP, "API1:2023 Broken Object Level Authorization", OWASP API Security Top 10. IDOR as the top API risk.
  3. OWASP, Web Security Testing Guide, Authorization Testing. The manual methodology this guide follows.
  4. OWASP, Authorization Cheat Sheet. Deny-by-default and enforce-server-side, in practice.
  5. PortSwigger, "Access control vulnerabilities and privilege escalation", Web Security Academy. Free labs covering horizontal, vertical, and IDOR cases.
All posts Application security

Beyond the blog

Want this tested on you?

Reading about it is one thing. Seeing it proven on your own systems is another.