Article

IDOR Vulnerability (BOLA): Why Scanners Miss It and How to Test With Two Accounts

An IDOR is when your app takes an identifier from the request and returns the object without checking the requester owns it — change /api/orders/1001 to 1002 and you read someone else's order. It is the most common serious web bug and the one scanners are worst at finding.

By Paul Rudenko, Security ResearcherUpdated Sep 21, 20268 min read

An IDOR — insecure direct object reference — is when your application takes an identifier from the request and returns the matching object without checking that this user is allowed to see it. Change /api/orders/1001 to 1002 and you read someone else's order. It is the most common serious bug in web applications and the one automated scanners are worst at finding.

What is an IDOR vulnerability?

The pattern is always the same three steps: the client sends an identifier, the server looks the object up by that identifier, and the server forgets to ask whether the requester owns it. Authentication passes — the attacker is a legitimate logged-in user — and authorisation never happens. In the OWASP API Security Top 10 this is API1:2023 Broken Object Level Authorization, listed first because it is both the most widespread and the most damaging.

GET /api/invoices/8842        Cookie: session=<attacker's own valid session>
→ 200 OK  { "customer": "someone.else@example.com", "total": 4210.00, ... }

Nothing about that request is malformed. No payload, no injection, no unusual header — just a different number. That is why a firewall does not stop it and a log review does not show it.

IDOR, BOLA — is there a difference?

Practically, no. “IDOR” is the older, broader name from the web-application era; “BOLA” (Broken Object Level Authorization) is what OWASP calls the same failure in its API-specific Top 10, where it is API1. Related siblings worth knowing: BFLA (Broken Function Level Authorization — a normal user reaching an admin action rather than another user's object) and excessive data exposure (the endpoint returns fields the client should never see, even for its own object).

Why do automated scanners miss IDOR?

Because a scanner has no idea who owns what. Tools like Nuclei, ZAP or a commercial DAST match patterns: a known CVE, a reflected payload, a missing header, an error string. An IDOR produces a perfectly normal 200 OK with valid business data — indistinguishable from a correct response unless you know that the data belongs to a different user. The check is semantic, not syntactic.

There is exactly one reliable way to detect it automatically: hold two real accounts at once and compare. Request an object as account A, record what comes back, then request the same object as account B and ask whether B received A's data. Everything else — single-account scanning, unauthenticated crawling, static analysis of routes — is guessing. That is also why most cheap “API security scanners” quietly do not test for it: it requires credentials, a data model and a comparison oracle rather than a payload list. See what the tools actually cover.

How do you test for IDOR properly?

Two accounts, one object, three questions. This is the method our engine automates, and it is the same method a good tester runs by hand:

  • 1. Cross-account read. Fetch an object as its owner (account A) and note identifying markers in the response — an email, a name, an account number. Fetch the same URL as account B. If B's response contains A's markers, the leak is confirmed, not suspected. Comparing whole response bodies is not enough on its own: two tenants often receive the same rows in a different order, so the comparison has to be an order-independent set comparison of the returned records, or you will report a clean result on a leaking API.
  • 2. Unauthenticated read. Fetch the same URL with no session at all. A 200 here is the most severe variant — the object is simply public.
  • 3. Bounded neighbour enumeration. If the identifier is sequential, try a small range around it as the owner. Being able to walk 1001 → 1002 → 1003 tells you the identifier is guessable and the object space is enumerable. Keep the range small and read-only: the goal is proof, not a data dump.
Common gotcha — the false positive that wastes a day: two test accounts in the same organisation, tenant or workspace are supposed to see the same data. We hit this on our own dogfood run: accounts A and B saw identical invoices and it looked like a cross-tenant leak, until it turned out both belonged to one organisation and row-level security was working exactly as designed. Before testing, make sure your two accounts are in genuinely separate tenants — and when the result says “B saw A's rows”, confirm the tenancy before you write it up.

How do you fix and prevent IDOR?

  • Filter by owner in the same query. Never look an object up by id and then check ownership as a second step — the two can drift apart, and the second step is what gets forgotten in a refactor. One query, both conditions.
  • Reuse one ownership lookup. A shared helper that every id-scoped route calls is the only version of this that survives a growing codebase; re-implementing the check per route guarantees one route eventually skips it.
  • Return the same 404 either way. If “not found” and “not yours” produce different responses — or different response times — an attacker can enumerate which objects exist without ever reading one.
  • Unguessable identifiers help, but are not the control. UUIDs raise the cost of enumeration; they do not authorise anything. An id that leaks through a shared link, a referrer header or a support ticket is still an id.
  • Test it in CI, with two accounts. The rule that holds up over time: every new/{id}-shaped route gets a test that requests it with a second account and expects a 404 (OWASP IDOR prevention cheat sheet).

We hold ourselves to the same rule — it is written into this product's own engineering constraints, and every id-scoped route in it filters by (id, current_user) together and returns an identical 404 for both cases.

For the site owner and for the developer

For the site owner (plain English)

An IDOR means one of your customers can see another customer's data by changing a number in the web address — no hacking tools, no password guessing. It is the bug behind a large share of the data-leak stories you read about, and it is invisible to the scanners most vendors run, because from the outside the response looks completely normal. The only way to know is for someone to hold two accounts on your system and try. If you handle customer records, orders, invoices or documents, this is the single test worth paying for.

Our deep audit runs exactly this: two accounts you create, cross-account and unauthenticated reads across your real endpoints, and proof in the report — 'logged in as B, read A's invoice' — rather than a severity label. Quoted per application, from €900.

Ask about a deep audit

For the developer

# The shape that is wrong — two steps that can drift apart
order = db.query(Order).filter(Order.id == order_id).one_or_none()
if order.user_id != current_user.id:      # forgotten in one route = IDOR
    raise NotFound()

# The shape that holds — one query, both conditions, identical 404
order = db.query(Order).filter(
    Order.id == order_id, Order.user_id == current_user.id
).one_or_none()
if order is None:
    raise NotFound()                      # same response whether it never existed or isn't yours

Then prove it, rather than assuming: register a second account and request the first account's ids against it, in a test that runs on every build. A code review cannot see a missing check; a cross-account request can. For the wider picture of what else to test on an API, see the API security testing guide and the API security checklist.

Frequently asked questions

What is the difference between IDOR and BOLA?

They name the same failure. IDOR — insecure direct object reference — is the older, broader term from web application security: the app accepts an identifier from the client and returns the matching object without verifying that this user may access it. BOLA — Broken Object Level Authorization — is what OWASP calls it in the API Security Top 10, where it sits at number one because APIs expose object identifiers constantly and often skip the ownership check. If a report says BOLA and another says IDOR, assume they mean the same thing and read the detail. Two related siblings are worth distinguishing: broken function level authorization (BFLA), where a normal user reaches an admin action rather than another user's object, and excessive data exposure, where the endpoint returns fields the client should never see even for its own record.

Why don't vulnerability scanners find IDOR?

Because a successful IDOR looks exactly like a successful request. Scanners work by matching patterns — a known CVE signature, a reflected payload, an error string, a missing header — and an IDOR produces a valid 200 OK containing real business data. Nothing in the response is anomalous unless you already know the data belongs to a different user, which is knowledge about your data model that a scanner does not have. Detecting it automatically requires holding two authenticated sessions at once and comparing what each receives for the same object, plus an oracle for deciding when a response 'belongs' to the other account. That is why single-account scanning, unauthenticated crawling and static route analysis all miss it, and why most inexpensive API scanners do not claim to cover OWASP API1 in any depth.

How do I test my own application for IDOR?

Create two accounts in genuinely separate tenants or organisations, then take an object that belongs to the first — an order, an invoice, a document, a user profile — and request its URL three ways. First as the owner, recording identifying markers in the response such as an email or account number. Second as the other account: if those markers appear, the leak is confirmed rather than suspected. Third with no session at all, which is the most severe variant. Then, if identifiers are sequential, try a small range around the original as the owner to see whether the object space is enumerable. Keep everything read-only, and only do this against systems you own or are contracted to test. The most common false positive is two accounts inside the same organisation, which are supposed to share data.

Do UUIDs prevent IDOR vulnerabilities?

No, they raise the cost of finding one. Replacing sequential integers with UUIDs means an attacker cannot simply increment an identifier to discover other objects, which removes the easiest enumeration path and is worth doing. It does not add an authorisation check: if the identifier leaks — through a shared link, a referrer header, a support ticket, an export, a mobile app's local storage, or another endpoint that lists ids — the object is still returned to whoever asks. Treat unguessable identifiers as defence in depth and the ownership filter in the query as the actual control. Anything that relies on the identifier being secret is relying on secrecy that the application itself hands out.

Related guides

See your whole external attack surface

One page is a start. The full external scan covers TLS, headers, DNS, exposed files, open services and known-exploited CVEs across your whole domain.

See the full scan →