BOLA: why broken object level authorization keeps breaking APIs
The number one entry on the OWASP API Security Top 10 is also the one automated tooling is structurally worst at finding. Here is how it works, why scanners miss it, and what testing for it actually looks like.
Broken Object Level Authorization is the most boring vulnerability class in modern application security and comfortably one of the most damaging. It is not clever. There is no memory corruption, no exotic parser abuse, no race condition. It is an API that hands you someone else’s data because you asked for it by number.
It has held the top position on the OWASP API Security Top 10 since that list existed, and it is behind a meaningful share of the mass data exposures that make the news. If you run an API and you have never specifically tested for it, assume you have it.
What it actually is
Almost every API exposes objects by identifier. An order, an invoice, a patient record, a message thread — each has an ID, and endpoints take that ID as a parameter.
BOLA occurs when the server verifies that you are logged in but never verifies that this particular object belongs to you. The authentication check passes. The authorization check — the object-level one — was never written.
That second response is the entire vulnerability. The token was valid, so the request was authenticated. Nobody asked whether user A had any business reading invoice 40319.
The reason this is so damaging is that it scales trivially. An attacker who finds one instance of it does not read one record — they iterate the identifier space and read all of them. A single missing check becomes a full database exfiltration.
Why scanners structurally cannot find it
This is not a gap that better tooling closes. It is a category problem, and it comes down to three things.
The request is completely valid
There is no payload. No injection, no malformed input, no anomalous header. The request is syntactically perfect and semantically ordinary — it is precisely the request the API was built to serve. Detection logic that looks for something wrong with the request has nothing to look at.
The response looks correct
A 200 with a well-formed invoice body is a success. To automated analysis it is indistinguishable from a legitimate response. Determining that it is a vulnerability requires knowing whose invoice it is — which requires knowing your data model.
It needs two identities to detect
The only reliable way to find BOLA is to hold at least two authenticated sessions belonging to different users, enumerate the objects visible to one, and then request them as the other. A scanner given a single set of credentials cannot perform that comparison. It has nothing to compare against.
This is the concrete reason a test needs credentials for every user role, not just one. A single-account engagement cannot test the object boundary at all — the highest-impact class of API flaw is silently out of scope before testing begins.
BOLA is not BFLA
These two get conflated constantly and they are separate checks.
BOLA is horizontal: you may perform this action, but not on this object. User A reading user B’s invoice.
BFLA — Broken Function Level Authorization — is vertical: you may not perform this action at all. A standard user reaching an administrative endpoint because the UI simply never showed them the button.
An API can be clean on one and broken on the other, so both need testing independently.
What manual testing looks like
Map the object surface
Walk the API and catalogue every endpoint that accepts an object identifier — in the path, in a query string, in a request body, in a header. Specifications help here but rarely tell the whole story; undocumented and legacy endpoints are frequently where the gaps live.
Establish two identities in the same role
Two ordinary accounts, ideally in separate tenants or organisations, each with their own data. This pair is what makes the boundary observable.
Cross the boundary, endpoint by endpoint
Capture an identifier belonging to account A, then request it while authenticated as account B. Repeat across every verb the endpoint supports — read access is frequently locked down while the corresponding update or delete path is not.
Check the indirect paths
Direct fetches are the obvious case and the one most likely to be handled. The gaps tend to sit in nested resources, bulk endpoints, export and reporting functions, webhook configuration, and anywhere a filter parameter is passed straight into a query.
Establish the actual impact
One accessible record is a finding. Demonstrating that the identifier space is enumerable and that the exposure covers the whole dataset is what turns it into the finding that gets prioritised. This is also where a report earns its keep — the severity is a function of what is reachable, not of the bug class.
Fixing it
The remediation is conceptually simple and organisationally hard: every request for an object must verify that the authenticated principal is entitled to that specific object, server-side, at the point of access.
What makes it hard is consistency. One developer forgetting the check on one endpoint reintroduces the vulnerability. So the durable fixes are architectural rather than per-endpoint:
- Centralise the check. A shared authorization layer that every data access path routes through, rather than a check copy-pasted into each controller.
- Scope queries by owner, not by ID alone. Retrieve by identifier and owning tenant in the same query, so an unauthorised fetch returns nothing rather than relying on a separate guard.
- Deny by default. New endpoints should be unreachable until authorization is explicitly declared.
- Prefer unpredictable identifiers. Random IDs are defence in depth, not a fix — they slow enumeration and do nothing about a leaked identifier. Never treat this as the control.
- Test the boundary in CI. Automated integration tests that assert account B cannot reach account A’s objects catch regressions between engagements.
That last point is the one worth investing in. A penetration test finds the instances that exist today. A cross-tenant assertion in your test suite stops the next one from shipping.
When was your API last tested?
Our API assessments test every object boundary manually, across every role. Tell us what you are running and we will scope it.
