August 13, 2026
Hunting IDOR & BOLA in REST APIs: A Practical Authorization Testing Methodology
A method for detecting broken authorization in REST APIs is shown using the OWASP crAPI.
By Ishant
5 min read
The way most people first encounter IDOR testing is by noticing a URL which has id=123, altering it to id=124 and then seeing what occurs; it often works so beginners come to the conclusion that this is all there is to the technique.
It's not. This kind of workaround is actually a sign of a more fundamental issue. When you go beyond a simple web application to a REST API involving nested resources, multiple roles, JWTs, and microservices communicating with each other, "just change the ID" ceases to be a method and becomes nothing more than a guess.
My approach to authorization testing comes from my hands-on experience with HackTheBox, PortSwigger, and OWASP crAPI, along with the OWASP API Security Top 10. While working with different applications and labs, I learned that effective authorization testing is not primarily about changing an ID. Rather, it requires understanding how objects, identities, roles, and authorization boundaries interact.
IDOR, BOLA, BFLA: Same Family, Different Bugs
IDOR is the original and general term; the app takes an identifier directly from the user and then uses it to retrieve or alter a resource without verifying that the person making the request actually owns it.
BOLA (Broken Object Level Authorization) is the API Security terminology for broken object-level authorization. It has the same fundamental flaw but appears instead as a vehicle_id, an order_id, or a report_id in an API request rather than as a link in HTML.
BFLA (Broken Function Level Authorization) is unlike the other cases since it isn't a question of object ownership but rather that a user with low privileges is able to access a function which is intended only for users with a higher role. You don't manage to obtain other people's data by means of an ID; instead, you're making use of a door that was never meant to be accessible to you.
A quick way I keep these straight:
- IDOR / BOLA → wrong object, right function
- BFLA → wrong function, doesn't matter whose object
Both results stem from the same root cause, which is that the backend trusts something — such as an ID, a role claim, or a hidden field — that it should be verifying independently against the authenticated user.
The Framework I Use
Instead of "find an ID, swap it," I break every request into five parts:
Actor → who is making this request
Mechanism → what action is being performed
Object → what resource is being acted on
Identifier → what value points to that object
Boundary → what should stop this actor from reaching this object
Once the situation is set out in this way, the testing plan becomes obvious. It's either the case that the Actor is able to reach an Object it shouldn't (BOLA) or that it can trigger a Mechanism it shouldn't (BFLA); all the other aspects, such as JWT decoding, multi-account setup, and parameter fuzzing, are just tools serving the purpose of that boundary check.
Step 1: Build an object and endpoint inventory
Before attacking anything, I browse the app with Burp's proxy running and just catalogue what objects exist, users, vehicles, orders, reports, and which HTTP methods touch each one:
Object: Vehicle
GET /identity/api/v2/vehicle/{id}
GET /identity/api/v2/vehicle/{id}/location
POST /identity/api/v2/vehicle/add
Although this step is slow, that is exactly how people end up testing the same three obvious endpoints and fail to discover those which are hidden in a settings page that no one checks.
Step 2: Find the identifier, don't assume it's the obvious one
At times the identifier is a simple sequential integer; at other times it's a UUID, which simply means that the number can't be guessed, not that it's protected. Occasionally the identifier that actually matters isn't in the URL but is instead contained in a JWT claim or a cookie.
To test this reliably, reduce the request to its minimal form by eliminating the headers, cookies, and body fields one by one; the particular field whose removal causes the request to stop working is then the one that acts as your true identifier. I once searched for a user_id in the request body, only to find that it was entirely decorative while the actual authorization decision was always being made based on a session cookie.
Step 3: Multi-account testing
I require a minimum of two full accounts, and when role-based testing is involved, a third account with elevated privileges can be useful. I use plus-addressing so that all the messages end up in one inbox.
you+accA@yourdomain.com
you+accB@yourdomain.com
you+admin@yourdomain.com
For Account A, record the requests and note down every object ID that it owns; do the same for Account B. Then, when using A's session, test each of B's identifiers against all of the CRUD operations from step 1:
Account A session + Account B's object ID
→ GET (may I read it?)
→ PUT (may I modify it?)
→ DELETE (may I remove it?)
→ POST (will the new object be attached to the wrong owner?)
Reading is usually the simplest case to test, and that's where many people stop. Updates and deletions can demonstrate a more direct impact because they show that an attacker may be able to modify or remove another user's data.
Where JWTs Fit In
A JWT is no more than another place where an identifier or an authorization decision could be hiding, rather than a distinct subject. When I encounter a token, the checklist is brief:
-
Carry out the decoding and check each claim (the sub, the user_id, the role, the custom fields).
-
Look at the algorithm, since symmetric ones are what attract my attention — HS256 in particular.
-
Check whether the signature is being properly validated by modifying the token and observing how the server responds. If the server continues to accept the modified token, this may indicate that the signature isn't being properly verified.
-
Before attempting to break the crypto, make sure that the token is actually being used in the decision you're testing if it has been validated.
That final point is important; when I previously pursued a JWT that appeared to be unvalidated, I soon realized the endpoint was not using it at all, instead extracting the identity from a different cookie. A JWT that looks odd is merely a lead, not a finding, until it has been confirmed as being relevant to that particular request.
Testing BFLA: Roles, Not Objects
In this case, you're not exchanging IDs between peers but rather checking if a member with a lower role can access a function of a member with a higher role.
-
Create accounts for each role that is available.
-
Log in using the lowest role and note the requests that it is permitted to make.
-
Log in with a higher level of access and capture requests to functions that the lower role's UI doesn't expose.
-
In the session associated with the lower role, reperform the requests made by the higher role.
The presence of a hidden button is of no use unless the backend separately verifies the role. It is also at this point that you come across the mechanisms which nobody included in the access control matrix, namely the "change user role" or "export all data" endpoints that were added later.
Don't Trust a 200 OK
A 200 OK status doesn't constitute proof of anything. In some cases, backends will return "success": true even if no real change has taken place. Before drawing any conclusions, send the request and then independently retrieve the object again to verify that the data actually changed. It's by skipping this step that false positives end up in the reports.
Checklist
SETUP
- Multiple accounts created (peer accounts + role variants)
- Object inventory built
- Identifiers mapped per object
BOLA / IDOR
- Cross-account read / update / delete / create
BFLA
- Low role attempting a high-role mechanism
- UI-hidden endpoints replayed directly
- Undocumented mechanisms tested specifically
JWT
- Decode and read every claim
- Confirm the token is actually used for the decision being tested
- Test signature validation before assuming it's broken
VERIFICATION
- Independently confirm object state changed
- Reproduce from a second, clean session
Closing Thought
The 'change the ID' method works since it provides a quick answer to an important question : does this application know who is asking or does it simply rely on what has been told it? All of the previous approach is a more gradual and careful way of posing that same question for each object, role, and request, rather than hoping that the first URL you examine will work.
I am looking at this category since it doesn't require the use of a scanning program, because it means you have to actually read an application, and a valid result can be explained in a single sentence. "I could see and edit other customers' data" lands a lot harder than a CVSS score ever will.