DEV Community

Cover image for 🔓 IDOR / BOLA: How to Find the #1 API Vulnerability — and Actually Prove It
Krishna Kumar
Krishna Kumar

Posted on

🔓 IDOR / BOLA: How to Find the #1 API Vulnerability — and Actually Prove It

Broken Object Level Authorization — IDOR, to most of us — is the single most common serious bug in modern APIs. It sits at #1 on the OWASP API Security Top 10 (2023), and it's the one I watch sail past a scanner, past a code review, and straight into production, where it lives for years.

Not because it's clever. Because it's boring: the application carefully checks who you are, and then forgets to check whether the thing you just asked for is actually yours.

This guide walks the whole path — what IDOR/BOLA is, how it works, how to find it, and the part most write-ups skip entirely: how to prove it, cleanly, without touching a single real user's data.

What you'll learn

By the end you'll be able to recognize where object-level authorization breaks, test an endpoint for it with two accounts you control, and — most importantly — turn "this looks vulnerable" into a report that triages instead of bouncing.

Prerequisites

Basic familiarity with HTTP requests, REST APIs, and how a bearer token or session cookie identifies you to a server. If you've ever opened your browser's dev tools and watched the Network tab, you're ready.

What is IDOR / BOLA?

Two names, one root cause.

  • IDOR (Insecure Direct Object Reference) is the classic bug-bounty name. An app exposes a reference to an internal object — /orders/1002, ?user_id=55, {"account":"ACME-9"} — and lets you tamper with it.
  • BOLA (Broken Object Level Authorization) is what OWASP calls the same thing on the API Security Top 10, where it ranks first.

The vulnerability is this: an endpoint accepts an identifier for some object and returns (or modifies) that object without checking whether the caller is allowed to touch it. The server confirmed you're logged in. It never confirmed the object is yours.

How it works: the technical details

Imagine an app that shows your invoice. With your valid bearer token, you request:

GET /api/v1/invoices/1002

You're invoice 1002. Out of curiosity, you change one digit:

GET /api/v1/invoices/1003

If the server responds 200 OK with someone else's invoice (same valid token of yours), that's IDOR. Your token was valid, so you passed authentication. The server never asked the only question that mattered — "does this user own invoice 1003?" — so it failed authorization.

That's the entire bug. Authentication asks who are you. Authorization asks are you allowed. IDOR is what happens when an app does the first and skips the second at the object level.

Authentication confirms who you are; authorization must confirm the object is yours. IDOR is when that second check is skipped — a valid token sails through and the response hands back another account's data.

Where IDOR hides

It's rarely as obvious as a number in the URL. Look for object references in every part of the request:

  • Sequential IDs — /users/55, /orders/1002. The easy case; just add or subtract.
  • UUIDs and random IDs — /files/3fa85f64-5717-.... A non-guessable ID is not an authorization control. If that UUID ever leaks — in a response, a referrer header, a shared link, another endpoint's output — the object is still unprotected.
  • The request body — {"account_id": 9} in a POST/PUT/PATCH.
  • Headers and cookies — X-Account-Id, tenant headers.
  • Nested and chained resources — /orgs/1/projects/44/secrets. The app checks your access to org 1 but not to project 44.
  • Non-GET methods — the juicy ones. A DELETE /comments/88 or PATCH /users/55/role that doesn't check ownership is a write-IDOR, often more severe than a read.
  • Blind / stateful IDOR — nothing useful comes back in the response, but the action still happened (you changed another user's setting). You prove these by side effect, not by the response body.

Detection is not proof

Here's where most hunters stop too early — and where reports get closed as "informational."

Detecting an IDOR is seeing that a different ID returns 200 instead of 403. That's a signal. It is not a finding.

Proving an IDOR is showing that the response contains data that belongs to a different account — and that you can reproduce it on demand. A 200 on its own could be an empty object, a decoy, a cached page, or your own data echoed back. The proof is the cross-account data, not the status code.

Before you write a single word of a report, make the finding survive three questions:

  1. Can you reproduce it on demand? Twice, from a clean session. A one-off 200 is a coincidence, and it will come back "cannot reproduce."
  2. Can you show impact, not just presence? "The endpoint returned 200" is presence. "Account A read account B's email, phone, and address" is impact.
  3. Is it in scope, and is your proof safe? Demonstrate with accounts you own. Never pull a real customer's record to "prove" it — that turns a bounty into an incident.

Reproduce, impact, scope. A finding that answers all three is real.

How to find it (and prove it safely)

The clean method needs two accounts you control — call them Alice and Bob. Both are yours, so any data you touch is your own test data. That single habit is what makes your proof both convincing and in-scope.

The two-account proof workflow: as Alice, request Bob's object; the response returning Bob's unique data under Alice's token is the proof — with Bob reading Bob's own object as the control.

  1. Log in as Bob. Create or open an object — an invoice, a profile, a note. Record its ID (BOB_ID) and one unique field you'll recognize later (say, bob+test@you.example).
  2. Log in as Alice and grab her token.
  3. As Alice, request Bob's object. Both accounts are yours, so $TOKEN_A is Alice's and the object belongs to Bob (also you):

curl -s https://api.target.example/api/v1/users/BOB_ID/profile -H "Authorization: Bearer $TOKEN_A"

A vulnerable server responds 200 OK with Bob's record:

{ "id": "BOB_ID", "email": "bob+test@you.example", "phone": "+1-555-0137" }

Alice's token just read Bob's profile. That response — Bob's unique email, returned under Alice's token — is the proof. Not that the endpoint "looks" authorizable. That it handed over another account's data, on demand.

Show the control for contrast, so there's no ambiguity — the same request with Bob's own token:

curl -s https://api.target.example/api/v1/users/BOB_ID/profile -H "Authorization: Bearer $TOKEN_B"

That returns 200 OK too — but it's Bob reading Bob, exactly what should happen. The gap between those two requests is the entire finding.

For a write-IDOR, prove it with a harmless, reversible change on your own second account — flip a display name on Bob's object using Alice's token, screenshot it, flip it back. You've demonstrated control without destroying anything.

If you're testing at scale, Burp Suite's Autorize or AuthMatrix extensions automate exactly this: they replay every request with a second account's session and flag the responses that shouldn't have succeeded. But the logic is the same two-account swap you can do with curl.

Write the report as proof, not a label

The report that gets paid is an equation, not a noun. Don't send "IDOR in profile endpoint." Send:

  • the exact request (method, path, which token),
  • the response, with the unique cross-account field highlighted,
  • the control request for contrast,
  • one line of impact: "Any authenticated user can read any other user's PII by changing the user ID — no further privileges required."

Same bug, two reports. One says IDOR in /users. The other shows Alice's token returning Bob's email, with the baseline next to it. The first gets closed. The second gets a bounty. The difference was never the bug — it was the proof.

How developers prevent it

  • Enforce object-level authorization on every object access. For every request that names an object, ask server-side: does this subject own, or have an explicit grant to, this specific object? Not "is this user logged in." Not "is this user an admin."
  • Centralize the check. Per-endpoint, hand-rolled ownership checks drift and get forgotten on the next new route. A single, mandatory authorization layer is far harder to bypass.
  • Don't lean on unguessable IDs. UUIDs raise the cost of discovery; they are not authorization. Treat them as usernames, not passwords.
  • Test across roles and tenants. Most BOLA ships because QA only ever tested with one account. Test cross-user and cross-tenant on purpose.

Common mistakes to avoid

  • Hunters: testing with a single account. If you only have one identity, you will never see IDOR — you need a second object you're not supposed to reach. And don't assume a UUID endpoint is safe; check whether that ID leaks anywhere else first.
  • Developers: confirming authentication and calling it a day; scattering ownership checks across controllers so one new endpoint quietly ships without one.

Automating the proof step

The two-account swap is mechanical, which means it's automatable — and that's exactly the problem my team works on with Xalgorix, an open-source AI pentester. Its methodology has a dedicated phase for object-level authorization that tests access across roles and tenants, and — true to the theme of this post — every finding is replayed with a proof payload and then re-verified by a separate pass before it ever reaches the report, non-destructively. You can self-host it (Apache-2.0) or run it on Xalgorix Cloud.

Disclosure: I work on Xalgorix. The two-account method above stands entirely on its own, with or without any tool.

Try it yourself

Practice only on targets you own or are explicitly authorized to test. These are built for it:

  • PortSwigger Web Security Academy — the free "Access control vulnerabilities" labs are the best structured introduction to IDOR anywhere.
  • OWASP crAPI ("Completely Ridiculous API") — a deliberately vulnerable API with real BOLA to find.
  • VAmPI — a small vulnerable REST API, great for practicing the two-account swap with curl.

Spin one up, create two accounts, and go read the account that isn't yours — then write the finding the way we did above.

Conclusion

IDOR/BOLA tops the API Top 10 not because it's hard to exploit, but because it's easy to miss: the app does the visible half of security — logging you in — and skips the quiet half — checking that the object is yours. Find it with two accounts. Prove it with cross-account data, not a status code. Report it as an equation, not a label.

Do that, and you stop sending findings that come back "informational" — and start sending the ones programs remember.


🔐 If this was useful, follow me for more hands-on web & API security write-ups.

🔗 Connect with me on LinkedIn: https://www.linkedin.com/in/xalgord/

Top comments (0)