BOLA in APIs: why it is still the #1 finding and how we chain it into other customers’ data
Broken Object Level Authorization has led the OWASP API Top 10 for years and remains the most common critical finding in our API pentests. The mechanism, the variants scanners miss, and what actually fixes it.

A number in the wrong place
GET /api/v2/invoices/48213 returns the logged-in user’s invoice. GET /api/v2/invoices/48212 returns another customer’s. No error, no log, no alert. That is BOLA — Broken Object Level Authorization — item API1 of the OWASP API Security Top 10, the same problem the web world used to call IDOR.
It is the most frequent critical finding in our API pentests, and not because teams are careless: it comes from a reasonable architectural decision (identify objects by ID) combined with a check that looks like it exists but lives in the wrong place.
Why scanners don’t find it
A scanner doesn’t know what is "yours" and what is "someone else’s". It sees two 200 responses with valid JSON and moves on. Detecting BOLA requires two authenticated contexts, an understanding of the data model, and the right question: should this user see this? That is manual work — or in-house tooling that carries identity context, which is how we do it.
The variants we find most
1. Sequential or predictable IDs
The classic. Incremental integer, timestamp, national ID, email in the URL. Enumerable in minutes.
2. The UUID that leaks somewhere else
"We use UUID v4, it can’t be guessed." True — but the customer’s UUID shows up in the /api/orders response, in the confirmation email link, in the public page’s HTML, in the webhook to a partner. Obscurity is not authorization.
3. Authorization on GET, not on PATCH
The team protected reads. Writes (PUT, PATCH, DELETE) use a different handler, written later, without the same middleware. Reading is bad; changing another customer’s delivery address or payout account is worse.
4. The ID in the body, not the URL
POST /api/transfer with {"from_account": "...", "to_account": "..."}. The backend trusts the client-supplied from_account instead of deriving it from the session. We have seen this in a regulated fintech.
5. GraphQL and batching
query { user(id: 48212) { email, documents { url } } }. Nested resolvers often check authorization only on the root node. With aliases and batching, thousands of objects are enumerated in a single request.
6. "Internal" or legacy endpoints
/api/v1/ stayed live after /v2/. Or /internal/export with no authentication because "only the admin frontend calls it". The frontend is not a security boundary.
7. Authorization per tenant, not per object
In multi-tenant SaaS, the check is whether the user belongs to company X — not whether the object belongs to company X. A user from X reads Y’s objects by swapping the ID.
How we chain it: from a 200 to business impact
A single BOLA is serious. What the report has to show is the path to real damage:
- Read another customer’s invoice → personal data (GDPR/CCPA).
- The invoice carries the document UUID → download of the PDF with bank details.
- Same pattern on
PATCH /api/users/{id}→ change the recovery email → account takeover. - With the account, access to
POST /api/payouts→ money movement.
Each step is a documented request/response in the report. The board does not need to understand IDOR; it needs to understand "any user could change any other user’s payout account".
What actually fixes it
- Derive identity from the session, never from input.
from_accountcomes from the token, not the JSON. - Authorize on the object, on every operation. One place (policy, middleware, repository) that answers "may this subject perform this action on this object?" — and every handler goes through it, including GraphQL resolvers and internal jobs.
- Scope in the query, not afterwards.
WHERE id = ? AND owner_id = ?in the database. Filtering in memory after fetching everything is an invitation to leak. - Automated authorization tests in CI: for every endpoint, two users, one object each, assert 403/404. That prevents regressions; it does not replace the pentest, which finds the paths nobody wrote a test for.
- Rate limiting and alerting on enumeration patterns — it doesn’t stop the attack, but it shortens the window.
- Retire legacy. Old API versions need a shutdown date, not neglect.
What to ask for in your next API pentest
Require testing with at least two users per role and with the OpenAPI/Swagger spec in hand (grey-box). Ask for explicit coverage of BOLA, BFLA (function-level authorization, API5), mass assignment (API3) and the undocumented endpoints recon turns up. And ask for chaining: a report with 30 isolated IDORs is worth less than one with the path to takeover.
How Pentest Machine tests this
Our API Pentest is driven by the OWASP API Top 10 and by your API specification, with multiple identities per role and in-house tooling that carries authorization context during enumeration — which lets us test thousands of object × user × operation combinations without losing the manual analysis of chains. Every finding ships with a reproducible PoC, business impact and the fix pattern, and the retest is included.