Application security
Broken object level authorisation: the API risk no scanner will find for you
The top entry on the OWASP API Security Top 10 returns a perfectly normal 200 to the wrong person. Why tooling cannot see it, and how the testing actually runs.
An application can show the right data to the right person on every screen and still hand over every record it holds to anyone who asks for one by number. The gap between what the interface displays and what the server independently verifies is where this class of bug lives, and it is the single most reliable finding in API work.
OWASP lists it as API1 on the API Security Top 10, under the title Broken Object Level Authorization. Everyone says BOLA. It has held the top position across editions of that list for the same reason it keeps appearing in API security testing: the defect is structural, not careless.
What the risk actually is
Nearly every API names things with an identifier carried in the request. An order number in a path segment, a customer reference in a query string, a document UUID inside a JSON body. Authentication settles who is asking. Object-level authorisation settles whether this particular caller is entitled to this particular object. BOLA is what happens when that second decision is missing, partial, or made on some endpoints and quietly skipped on others.
The structural part is that object-level checks cannot be centralised the way authentication can. One piece of middleware can validate a token for every route in a service. No middleware can know that order 8412 belongs to tenant 17 without asking the domain a question. So the check gets written again inside every handler that touches a record, by whichever engineer writes that handler, on whatever day they write it. One omission in two hundred endpoints is a data incident.
Authentication is implemented once and applies everywhere. Object-level authorisation is implemented once per endpoint, and nothing fails loudly on the day somebody forgets.
Why tooling cannot find it
Automated security tools work by recognising a signature. A stack trace in an error body, a payload reflected back into the page, a timing difference, a version banner matched against an advisory. A BOLA response carries no signature at all. It is a 200 with a well-formed body, structurally identical to the 200 the endpoint returns to the account that legitimately owns the record.
To classify that response a tool would need two facts it does not hold: which account issued the request, and which account the returned object belongs to. Intercepting proxies such as Burp Suite and OWASP ZAP make capturing, editing and replaying those requests fast, and that is genuinely most of the mechanical labour. The judgement about whether a particular answer was permitted stays with a person who is holding both halves of the expectation in their head.
Write the matrix before writing the tests
Testing for broken access control is impossible to specify until somebody states what unbroken access control would look like. That statement is a matrix, and producing it is usually the first useful thing an engagement does, because most teams have never written one down.
- Every role the product recognises, including the ones nobody thinks of as roles: the support agent, the read-only auditor, the API key issued to a partner integration, the machine account behind a webhook.
- Every object type that has an owner. Orders, documents, invoices, exports, uploaded files, saved reports, audit entries, notification preferences.
- Every operation available on each object type, separated properly. Read, list, update, delete and export are five different decisions and are frequently implemented in five different places.
- The rule that ought to apply in each cell, written as a sentence a developer and a product owner both agree with before any request is sent.
Two directions, both worth testing
The horizontal direction is one account reaching another account’s object at the same privilege level. Customer A retrieving customer B’s invoice, or tenant 17 exporting tenant 18’s user list. This is where the commercially damaging findings sit, particularly in multi-tenant products.
The vertical direction is a lower-privileged account reaching a higher-privileged operation. OWASP separates that case out as API5, Broken Function Level Authorization, because the failure is about the endpoint rather than the object. In practice you test both in the same pass with the same accounts, and the distinction only matters when you write the finding up.
How the testing runs
- 01Provision at least two accounts per role, plus a second tenant. One account per role cannot demonstrate a horizontal failure, and provisioning the second one mid-engagement always takes longer than anyone expects.
- 02Drive the product normally through a proxy while capturing traffic, so the request set reflects what the application genuinely does rather than what the specification says it does.
- 03Replay each captured request with the other account’s session and the original identifier, then with the original session and the other account’s identifier. Record the status code, and record what came back in the body, because a 200 containing an empty result is a different finding from a 200 containing somebody else’s data.
- 04Repeat across every verb the endpoint accepts. Read access is frequently blocked while the update path on the same resource is not, and delete is often the one nobody tested.
- 05Follow identifiers into places that do not look like endpoints. Export and reporting routes, PDF and print generators, webhook callbacks, notification preferences, bulk and batch operations, GraphQL node lookups, and any earlier API version still answering on the same host.
The last of those is where the older findings hide. Version 1 of an API rarely gets decommissioned on the day version 2 ships, and the authorisation work usually went into version 2 only.
The variant that hides inside a correct response
Object-level checks can pass while the fields inside the object still leak. OWASP files this separately as API3, Broken Object Property Level Authorization, and it has two halves that are worth testing separately.
On the way out, the response serialises the whole database row and lets the client decide what to display. Internal cost prices, moderation flags, other users’ email addresses and password reset tokens all travel to browsers that never render them. On the way in, the endpoint binds whatever fields arrive onto the model, so a request that adds a role field or a tenant identifier gets it accepted because nobody wrote an allow-list.
What a durable fix looks like
- Derive the ownership scope from the authenticated session rather than from anything in the request. If the query is written so that it can only ever return rows belonging to the caller, the check cannot be forgotten on a new endpoint.
- Push the decision down to the data access layer, so that fetching a record requires passing the actor. A repository method that will not compile without an actor is a stronger guarantee than a code review convention.
- Deny by default, and make the default visible. An endpoint that returns data because no rule matched is an endpoint waiting for a rule to be forgotten.
- Treat unguessable identifiers as a delaying tactic, not a control. Identifiers leak through shared links, invoices, email footers, support tickets, exports and other endpoints, and they cannot be rotated once they have.
- Keep one negative case per object type in the regression suite. A cheap authenticated API test asserting a 403 for the wrong tenant costs almost nothing to maintain and catches the reintroduction that a one-off assessment cannot.
For a multi-tenant product this is the boundary the whole commercial proposition rests on, which is why SaaS teams tend to find it first in an enterprise security questionnaire rather than in their own test suite. The questionnaire asks whether tenant isolation is tested. Answering honestly requires having pointed one tenant at another and written down what happened.
Quick answers
Common questions
Is BOLA the same thing as an IDOR?
Substantially, yes. Insecure direct object reference is the older name for the same defect, framed around web applications; broken object level authorisation is the API-centric framing OWASP adopted for its API list. If a report you receive uses one term and a questionnaire uses the other, they are asking about the same failure: the server accepted an identifier without checking whether the caller was entitled to it.
Will switching to random UUIDs fix it?
No. Replacing sequential integers with UUIDs raises the cost of guessing an identifier, which is worth doing, but it is not an access-control decision. Identifiers escape constantly through shared links, exports, email, support tickets, browser history and other endpoints of your own API, and an identifier that has escaped cannot be rotated. Treat unpredictability as defence in depth behind a real check, never as the check itself.
How many test accounts do you actually need?
Two per role at minimum, and at least two tenants for a multi-tenant product. One account per role can only demonstrate vertical failures, where a lower privilege level reaches a higher-privileged function. The horizontal case, where one customer reaches another customer’s records at the same privilege level, is where the damaging findings usually are, and it cannot be demonstrated without a second account at the same level.
Can this be covered by automated regression tests afterwards?
The reintroduction case can, and cheaply. Once an assessment has established the authorisation matrix, a small set of authenticated negative tests asserting the correct rejection for each object type will catch a new endpoint that forgot the check. What automation will not do is find the failure the first time, because it has no way to know what the correct answer was supposed to be until somebody writes it down.