The bug in one sentence

Broken object-level authorisation, still filed under the older name IDOR, is what happens when an endpoint accepts an identifier from the request and fetches the object it names without checking that the caller is allowed to see it. The server authenticates the request — it knows who you are — and then skips the second question: is this object yours?

It is the most common API vulnerability class, and it is common precisely because it is easy to write. A handler that looks up a record by a path parameter reads as correct code. Nothing in it looks like a bug.

Set up the experiment properly

The single most common mistake is testing with one account. If you request /api/orders/1041 as the only user you have, and it returns an order, you have learned nothing — that order may simply be yours. You need two accounts, and you need to be clear which is which:

  • Owner — the account that legitimately created the object.
  • Attacker — a second, fully separate account, on the same tenant if the system has tenants.

Create a real object as the owner. Note its identifier. Then request that exact identifier as the attacker. That single swap is the whole test. Everything else is about interpreting the answer.

Read the body, not the status

The status code is a hint, not the verdict. The three cases that matter:

  • 403 or 404 as the attacker — the control works. A 404 rather than 403 usually means the server is deliberately hiding existence, which is fine.
  • 200 with another user's data — that is the bug. Quote the field that proves ownership, such as an email, name or account number.
  • 200 with an empty object, a redacted record, or a generic error body — that is not a finding. The server answered, but it did not disclose or change anything belonging to someone else.

The third case is where most false reports come from. A successful status on another user's identifier is only interesting if the response contains that user's data or the request alters that user's state.

The variants worth trying

Once the basic swap is understood, the same idea generalises. Each of these is the same bug wearing a different identifier:

  • Identifier guessing — sequential integers are the easy case; UUIDs are not protection if they appear anywhere another user can read, such as in a shared link or an export.
  • Nested routes — /api/users/1041/orders/9. The outer id may be validated while the inner one is trusted.
  • Body versus path — the path may be checked while a user_id in the JSON body is trusted.
  • Mass assignment — sending the owner's identifier inside an update payload and having the server apply it.
  • Function level — the same idea applied to an action rather than an object: an administrative endpoint that authenticates but never checks the role.
  • Method swaps — a read that is properly protected while the same URL under DELETE or PUT is not.

Turn it into a matrix, not a hunch

Endpoints multiply faster than manual tests. The reliable approach is to capture the real traffic a session produces, then replay each request under a different identity and diff the answers mechanically. Hugin's access-control audit does exactly this: it takes captured flows, replays them as another user and as no user at all, and flags the requests whose responses differ in a way that indicates data was disclosed or changed.

That is the same experiment as above, run across every endpoint you have already exercised, which is the difference between finding one bug and finding the class.