October 19, 2026
The User Changed One ID. I Tested What the Server Actually Trusted.
An IDOR investigation that started with one HTTP request and ended with a much better question.
By clipper
5 min read
I used to think the interesting part of testing an API was finding the endpoint.
A request containing /api/users/1042/orders/781 looked promising. The application accepted an order ID, returned JSON, and behaved exactly as expected. I could see the order details, the status, and the information associated with it.
Nothing looked broken.
But one question bothered me.
Was the server returning this order because I was allowed to see it, or simply because I had asked for it?
Those are two very different things.
I decided to stop looking at the response for a moment and start looking at the decision behind it.
The first request looked completely normal
I was working through an authorized web application testing scenario with two ordinary user accounts. Both accounts had their own orders, and neither account was supposed to access the other's private information.
I logged in as User A and captured a request in Burp Suite.
GET /api/orders/781 HTTP/1.1
Host: lab.example
Cookie: session=USER_A_SESSION
Accept: application/jsonGET /api/orders/781 HTTP/1.1
Host: lab.example
Cookie: session=USER_A_SESSION
Accept: application/jsonThe server returned 200 OK.
The response contained an order belonging to User A. That was expected. A successful response, by itself, told me nothing about whether authorization was implemented correctly.
I could have changed the ID immediately and celebrated if something unexpected appeared.
Instead, I needed a baseline.
I recorded which account was authenticated, which object it owned, and what the application normally returned. Then I logged in as User B and confirmed that User B had a separate order.
This small step mattered. Without two known accounts and two known objects, I would have had no reliable way to distinguish an access-control failure from normal application behavior.
Then I changed one number
I returned to User A's request and changed the order ID to the ID of User B's order.
I sent the request again.
There was no complicated payload. No unusual header. No injection string. Just a different object identifier in an otherwise ordinary request.
At this point, I wasn't interested in whether the number was predictable.
I wanted to know whether the server checked who owned the object.
An application can use sequential IDs, UUIDs, random identifiers, or a combination of them. None of those choices replaces authorization. A difficult-to-guess identifier might reduce discoverability, but it does not establish permission.
The important question was whether User A could retrieve an object that belonged exclusively to User B.
That is where the investigation actually begins.
A response code is not a verdict
Imagine three possible results from the modified request.
Case one: 403 Forbidden.
The server denies access. That is a good sign, but I would still examine the response and the application's behavior to make sure the denial is genuine and consistent.
Case two: 404 Not Found.
The object might not exist, or the application might intentionally hide objects that the current user cannot access. A 404 does not automatically prove that authorization is correct.
Case three: 200 OK.
This is where I would pay closer attention. But even a successful response does not establish a vulnerability on its own. The returned data matters.
If the response contains User B's private order details while the request is authenticated as User A, and User A has no legitimate permission to access that order, I have evidence of a potential broken object-level authorization flaw.
I need to verify ownership, access expectations, and the sensitivity of the exposed information before making that claim.
The status code tells me how the request ended. The response tells me what happened. The authorization rules tell me whether it was a vulnerability.
That distinction prevents a lot of false positives.
I stopped testing the number and started testing the permission
Once I had a clear baseline, I thought about the other ways an application might enforce access control.
A server might protect the order-detail endpoint but forget to protect a related invoice endpoint. It might check permissions when displaying a page but fail to repeat the check when an API request arrives directly. It might enforce ownership on reads but overlook updates or downloads.
These are not separate tricks for the sake of collecting more test cases. They are different places where the same authorization rule can fail.
In an authorized lab, I would organize the investigation around a few questions:
- Can User A read an object owned by User B?
- Can User A modify or delete an object owned by User B?
- Can a regular user access an action reserved for a different role?
- Do related endpoints enforce the same ownership and permission rules?
- Does the server reject unauthorized requests even when the browser interface never exposes the action?
Each request should answer a specific question. If I change five things at once, I make the result harder to explain and reproduce.
I also avoid assuming that access to one object implies access to every object of the same type. A support agent, account administrator, or shared-team member might have legitimate access under the application's business rules. The expected permission model matters.
The mistake I wanted to avoid
The easiest way to fool yourself during security testing is to confuse an unusual response with a confirmed vulnerability.
An endpoint returns extra fields. Is that sensitive information, or data the current user is entitled to see?
An identifier can be changed. Does the server expose another user's record, or does it reject the request before returning anything useful?
An administrator-only function appears in the API. Can a normal user actually execute it without permission?
These questions require evidence, not intuition.
For an IDOR finding, I want to establish four things:
- Identity: Which account is making the request?
- Ownership: Who owns the target object?
- Permission: What access should the requesting account have?
- Impact: What information or action becomes available without authorization?
When those pieces line up, the report becomes much stronger. Another tester can reproduce the behavior, a developer can understand the failed security rule, and the organization can assess the actual risk.
Without that evidence, I might have a suspicious observation, but I don't yet have a defensible finding.
What the server should have checked
The fix is not simply to replace sequential IDs with random ones.
The server should authenticate the user, identify the requested resource, and enforce the applicable authorization policy before returning data or performing an action.
For a resource owned by one user, a database query might constrain the lookup to both the requested object and the authenticated user's identity:
SELECT *
FROM orders
WHERE id = :order_id
AND owner_id = :authenticated_user_id;SELECT *
FROM orders
WHERE id = :order_id
AND owner_id = :authenticated_user_id;The placeholders must be bound parameters, and the authenticated user ID must come from trusted server-side authentication context โ not from an untrusted value supplied by the client.
This is a simplified example, not a complete authorization framework. Applications with teams, delegated access, shared resources, or administrative roles need to enforce their actual permission model rather than blindly requiring individual ownership.
The same principle applies to updates and deletes. Checking access when retrieving a page is not enough if a separate API endpoint can modify the underlying object.
Authorization needs to be enforced server-side on every relevant operation.
The question I use now
Changing an ID is easy. Understanding why the server accepts it is the difficult part.
When I encounter an endpoint containing an object identifier, I try not to ask, Can I change this value?
I ask:
Who is allowed to access this object, what rule establishes that permission, and where does the server enforce it?
That question changes the way I approach IDOR and BOLA testing. Instead of collecting endpoints and hoping one behaves strangely, I establish ownership, test a specific authorization boundary, and look for evidence that the boundary has failed.
The difference is not a more complicated payload or a larger toolset.
It is a better question asked at the right point in the investigation.
One changed ID can reveal a serious flaw. But only evidence of broken authorization turns that observation into a security finding.
That is the part worth spending time on.