
Broken Object Level Authorization sits at the top of the OWASP API Security Top 10 for a reason. It is simultaneously the most common serious API flaw and the one automated tooling is least able to help with. Understanding why the second thing is true tells you a great deal about what an API assessment has to involve.
The shape of the flaw
An API endpoint accepts an object identifier — an invoice number, a user ID, a document reference — and returns the object. Authentication works correctly: the caller presents a valid token and the API verifies it. What the API fails to do is check that this particular caller is entitled to that particular object.
Change the identifier, get someone else’s data. That is the entire attack.
GET /api/v2/cards/8841 Authorization: Bearer <valid token>
GET /api/v2/cards/8842 Authorization: Bearer <same valid token>
If the second request returns a card belonging to a different user, you have BOLA. No payload, no encoding trick, no injection. A number was changed.
Why scanners cannot find it
This is the part worth internalising, because it drives how you should scope an assessment.
A scanner sends the second request and receives HTTP 200 with a well-formed JSON body. From the scanner’s position that is a correct response to a valid request. There is no error, no anomaly, no signature. Nothing in the response says “this data belongs to someone else,” because the API itself does not know that.
Determining that the response is wrong requires knowing which objects the caller should be able to reach. That is business context, and it lives in your head and your specification, not in the traffic.
The consequence: BOLA is only findable by a tester who holds at least two accounts and knows which objects belong to which. This is why we ask for credentials at every permission level and, in multi-tenant systems, in at least two tenants. Without that second account, tenant isolation is not testable at all — not slowly, not partially. It is invisible.
How it is tested properly
The method is unglamorous and it works.
- Enumerate objects per account. Log in as account A, exercise the application, and record every object identifier it legitimately touches. Repeat as account B in a different tenant.
- Cross the streams. Replay A’s requests with B’s session, and B’s with A’s. Every endpoint, every method — not just GET. Write operations are frequently less protected than reads, because the developer reasoned that nobody would guess the ID.
- Test every role pairing. Not just tenant-to-tenant. A support agent reading a customer’s record may be intended; a support agent in one tenant reading a customer in another is not.
- Check indirect exposure. Object identifiers leak through search results, export functions, notification payloads and error messages. An endpoint that is properly authorised is still a problem if a different endpoint hands out the identifiers it protects.
- Look at the object graph.
/cards/8841may be protected while/users/12/cardsis not, or while a GraphQL query can traverse to the same object by a different path.
Where the fix belongs
The recurring pattern in remediation is that authorisation checks are scattered across individual controllers. Each one is written by a different developer at a different time, and the coverage is necessarily uneven — a single missed endpoint reopens the whole class.
The durable fix is a single server-side authorisation layer that resolves the caller’s identity and tenant from the session, and applies that scope to every object lookup, rather than trusting an identifier supplied by the client. Where that layer exists, BOLA becomes a bug in one place instead of a permanent property of the codebase.
Indirect object references — UUIDs in place of sequential integers — raise the cost of discovery but do not fix the flaw. If the identifier leaks anywhere, the object is still reachable. Treat them as defence in depth, not as a control.
What good evidence looks like
A BOLA finding in a report should show the exact request made as account A, the exact response containing account B’s data, and a statement of how many records were reachable. It should not contain the records themselves. Where we prove cross-tenant access, we count and classify the exposed data and stop — the count is what your risk team needs, and copying the records out creates a liability that outlasts the engagement.