Broken Object-Level Authorization (BOLA / IDOR): the API bug we find most
A BOLA vulnerability (API1:2023, IDOR) lets one user read another user's data by swapping an object ID. Here is how we find it and how to fix it.

Change one number in a URL and you are reading someone else’s invoice. That is the whole trick behind a BOLA vulnerability, and it is the finding we write up more than any other on API engagements. The endpoint behaves perfectly for the logged-in user. It returns clean JSON, passes every functional test, and then quietly hands over any record you ask for as long as you know the ID.
Broken Object-Level Authorization (BOLA) is the API-era name for what most engineers still call IDOR, an Insecure Direct Object Reference. It sits at the top of the OWASP API Security Top 10 as API1:2023, and the ranking is earned. It is easy to ship, it survives code review, and it is invisible to a scanner that has no idea what your data model looks like. Across the REST and GraphQL backends we test, this one class produces a big share of our high and critical findings.
The upside is that a BOLA vulnerability is also one of the most preventable bugs on the list once you know the shape of it. This post covers what it is, how we hunt it on a live engagement, what the raw request and response look like, the damage it does in production, and the fix that actually holds up.
Key takeaways
- A BOLA vulnerability (also called IDOR) happens when an API returns or modifies an object based on a client-supplied ID without checking that the caller is allowed to touch that object.
- It is OWASP API1:2023 (Broken Object Level Authorization) and maps to CWE-639, Authorization Bypass Through User-Controlled Key.
- We find it by authenticating as two separate accounts and replaying account A’s requests with account B’s token, swapping object IDs and watching for 200s that leak the wrong user’s data.
- Sequential integer IDs make mass enumeration trivial, but UUIDs and hashes do not fix the bug. They only slow the guessing down.
- The only reliable fix is a server-side authorization check on every object, on every request, keyed to the authenticated session rather than any ID the client sends.
What is a BOLA vulnerability (and how is it different from IDOR)?
A BOLA vulnerability is a failure to confirm that the authenticated caller is allowed to act on the specific object they asked for. The API authenticates you just fine. It knows exactly who you are. Then it takes an object ID straight from the request and skips the one question that matters: does this object belong to you?
IDOR and BOLA describe the same root cause. IDOR is the older, broader web term. BOLA is the name OWASP picked for the object-level flavor when it built the API Security Top 10, because APIs expose object identifiers everywhere and the pattern became the dominant API risk. When we write “IDOR” in a report, we mean exactly this: the object reference is direct, user-controlled, and unguarded. We use both terms for the same finding throughout this article.
It helps to separate BOLA from its cousin, Broken Function Level Authorization (API5:2023). Function-level bugs are about calling an action you should not be able to call at all, like reaching an admin-only route. Object-level bugs are about calling a perfectly legitimate endpoint on data that is not yours. You can have every route locked down and still bleed every customer record through one unchecked id parameter.
Why are REST and GraphQL APIs so prone to it?
APIs are prone to BOLA because they are built around addressable objects, and every one of those objects needs its own guard. A classic server-rendered app tends to scope data to the session by accident. The page only ever queries “my” orders, so there is nothing to fetch by arbitrary ID. An API does the opposite. It publishes a uniform surface like /api/orders/{id} and invites the client to name the object directly. Multiply that by a few hundred endpoints and a few dozen object types, and the odds that one of them forgot its ownership check climb fast.
REST puts the identifier right out in the open. It sits in the path or a query string, it is often a sequential integer, and it takes about ten seconds to fuzz. GraphQL hides the same problem in a different place. A single endpoint accepts an id argument on nested resolvers, and authorization has to be enforced at the field and node level. Developers who guard the top-level query routinely miss the nested node(id: ...) lookups, batched aliases, or connection edges that resolve objects on their own. The outcome is identical: pass an ID you do not own, get data you should not see.
Microservices stack one more failure mode on top. Once a request crosses an internal trust boundary, downstream services tend to assume the gateway already handled authorization, so they cheerfully resolve any object ID that lands in front of them.
How do we hunt a BOLA vulnerability on an engagement?
Our method is deliberately boring: authenticate as two separate low-privilege accounts, then try to make one account touch the other’s data. Boring is what finds real bugs consistently. Here is the loop.
First we map the object surface. We proxy the app through Burp Suite, click through every feature as user A, and catalog each endpoint that takes an object identifier. Order IDs, document IDs, message threads, invoice numbers, profile handles, file keys. Anything the client gets to name goes on the list.
Next we characterize the identifiers. Sequential integers mean an attacker can walk the whole dataset by counting up from 1. UUIDs, ULIDs, and hashes raise the effort, and we treat them as a speed bump, not a control. We keep finding “unguessable” IDs leaking through some other channel: a list view, an email receipt, a referral link, a GraphQL edge, a stack trace in an error response. Once the ID is exposed anywhere, its entropy stops mattering.
Then comes the two-account swap. We capture user A’s requests and replay them with user B’s session token, changing only the object ID so it points back at A’s resource. This is where the Autorize extension for Burp earns its slot in the toolbar. Feed it user B’s cookie or bearer token and it silently repeats every request you make as B, then tags the responses green (enforced), red (bypassed), or orange when it cannot decide and wants a human to look. A red row with A’s data in B’s response is a BOLA vulnerability, full stop.
We never stop at GET. Some of the ugliest findings we ship are on PUT, PATCH, and DELETE, where a missing check lets one user overwrite or destroy another’s records. We also move the identifier around: an ID that is checked in the URL is sometimes ignored when the same value is dropped into a JSON body, and a mass-assignment field like "userId" can quietly re-scope the object to someone else. For enumeration at scale we drive the ID parameter with ffuf or Burp Intruder and diff status codes and response lengths to spot which IDs return real data versus a uniform 404.
What does the vulnerable request actually look like?
Here is an anonymized example against a fictional target. User B is authenticated with their own valid token, but requests order 1024, which belongs to user A.
GET /api/orders/1024 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.userB-token
Accept: application/json
A safe API answers 404 Not Found or 403 Forbidden, because order 1024 is not scoped to user B. A vulnerable one authenticates the token, looks the order up by ID alone, and serializes it straight back:
HTTP/1.1 200 OK
Content-Type: application/json
{
"orderId": 1024,
"customer": "Alice Nguyen",
"email": "[email protected]",
"shippingAddress": "742 Evergreen Terrace",
"items": [ { "sku": "SKU-88213", "qty": 2 } ],
"total": "184.00",
"paymentLast4": "4242"
}
The token is user B’s. The data is user A’s. Nothing about the response is malformed. The server did precisely what the code told it to do, which was fetch order 1024 and return it. Swap 1024 for 1025, then 1026, and you have scripted your way through the entire order table. That is the bug in its purest form.
What is the real-world impact?
The impact is mass data exposure, and often account takeover riding on top of it. Because enumeration is scriptable, one unguarded endpoint does not leak a single record. It leaks all of them. We have seen object-level flaws expose full customer PII, order histories, payment metadata, private messages, and internal documents, all reachable by a loop that counts through IDs while you go make coffee.
It rarely stops at reading. If the same pattern lives on a profile or settings endpoint that accepts PATCH, an attacker can change another user’s email or phone number, then fire a password reset to the address they now control, and the read bug becomes a full account takeover. If the object is an API key, a webhook secret, or an invitation token, the leak turns into a foothold for deeper access. Regulators treat unauthorized access to personal data as a breach no matter how trivial the technique was, so the disclosure and compliance bill arrives the same as it would for any other exposure.
How do you prevent a BOLA vulnerability?
Enforce a server-side authorization check on every object, on every request, keyed to the authenticated session rather than anything the client sends. That one sentence is the fix. The rest is just how you make it reliable.
- Deny by default. Object access should fail closed unless an explicit rule says the current principal may see it. A new endpoint should inherit the block, not inherit access.
- Never treat a client-supplied ID as an authorization decision. The ID tells you which object. The session tells you who is asking. Authorization is the join between the two, and that join belongs on the server every single time.
- Scope the query to the owner. The most durable pattern we recommend is object-scoped data access: query
WHERE order.id = :id AND order.owner_id = :currentUserinstead of fetching by ID and checking ownership afterward. A row that is not yours simply never loads, so there is no gap between load and check for anyone to forget. - Centralize the check. Put authorization in a policy layer or middleware that every resolver and controller runs through, rather than copy-pasting an ownership check into each handler. Scattered checks are exactly how one endpoint gets missed.
- In GraphQL, authorize at the node and field level. Guard every resolver that loads an object by ID, including nested
nodelookups, connection edges, and batched aliases, not just the top-level query. - Use hard-to-guess IDs as defense in depth, not as the control. UUIDs cut down blind enumeration, but they never stand in for the ownership check.
Bake in regression coverage while you are there. A test that logs in as user B and asserts a 403 or 404 on user A’s object costs a few minutes to write and catches the day someone refactors the query and quietly drops the scope.
How CyberXplore helps
Object-level authorization is exactly the kind of flaw automated scanners miss and manual testing catches, because it turns on understanding your data model and running the two-account comparison against real business logic. Our API penetration testing team maps every object-bearing endpoint, replays requests across separate accounts, and validates each finding with a reproducible request like the one above, then hands you fix guidance to close it. If you want a BOLA-focused assessment of your REST or GraphQL API, request a quote and we will scope it around your actual endpoints.
FAQ
Is BOLA the same as IDOR?
Effectively yes. IDOR (Insecure Direct Object Reference) is the older, general web term, and BOLA (Broken Object Level Authorization) is the name OWASP uses for the object-level version in the API Security Top 10. They share the same root cause: the server acts on a client-supplied object ID without confirming the caller is authorized for that object. We use both terms for the same finding.
Where does BOLA sit in the OWASP API Security Top 10?
It is API1:2023, Broken Object Level Authorization, the number one entry in the 2023 list. It also maps to CWE-639, Authorization Bypass Through User-Controlled Key. Citing both in a report gives developers a precise, standards-backed reference for the class of bug and its remediation.
Do UUIDs prevent IDOR API bugs?
No. Random UUIDs make blind enumeration much harder, so they help against a drive-by attacker guessing IDs, but they add no authorization. If the ID leaks through a list endpoint, an email, a referral link, or a GraphQL edge, which it frequently does, the object is still wide open. Treat unguessable IDs as defense in depth, never as the access control itself.
Can a vulnerability scanner find BOLA?
Rarely on its own. Scanners do not know which object belongs to which user, so they cannot tell that a valid-looking 200 is actually leaking another account’s data. Finding BOLA reliably takes authenticated testing with at least two accounts and a tool like Burp’s Autorize to compare responses. That is why it stays near the top of the findings on manual API tests.
Does BOLA only affect GET requests?
No, and the write operations are often worse. A missing object check on PUT, PATCH, or DELETE lets an attacker modify or delete records that belong to other users, and a writable field like an email address can chain into account takeover. We test every method against every object-bearing endpoint, not just the reads.
How quickly can we fix a confirmed BOLA finding?
Often within a sprint. The most robust fix is to scope every data query to the authenticated owner so unauthorized rows never load, and to centralize that check in a policy layer instead of per-handler code. Add a regression test that asserts a 403 or 404 when one account requests another’s object, and the class stays closed as the code keeps changing.



