AppSec · May 6, 2026 · 6 min read

BOLA: the API breach you cannot see in the code

Broken object-level authorization is the top API risk - and it looks perfectly correct on the page that reads it.

The most common API breach is mundane. An endpoint returns /orders/1001 to the user who owns it, and it also returns /orders/1002 to a user who does not. The code reads fine in review. The route exists for a legitimate reason. Nothing is missing on the screen. The authorization check is simply absent, and absence leaves no error message, no red flag, no 500. Broken object-level authorization - BOLA, the number-one risk on the OWASP API Top 10 - is invisible precisely because everything that surrounds it behaves correctly.

The breach is not in the status code. It is in whose data came back. Both requests return 200, both render a tidy order page, and both look identical to a scanner that judges the surface. The only thing that separates a non-issue from a confirmed cross-tenant read is the body of the response, and that is the one place most tools never look.

01Why BOLA hides in plain sight

Object-level authorization is a per-request decision: for this caller, is this specific object theirs to see? The decision lives in one server-side check, and when that check is missing the application does not break. It does the opposite - it works perfectly, returning whatever object id you asked for. There is no stack trace to catch, no malformed page to notice, no missing field to flag. A single-page app compounds the illusion: it paints the same shell for every order id, and the data arrives over a separate API call the eye never inspects.

Source review does not save you either. The handler that fetches an order by id can be clean, well-typed and well-tested, and still ship without the ownership clause in the query. The bug is a line that was never written, and you cannot grep for a line that is not there. This is why object-level authorization cannot be judged from source alone or from a 200 response. Only the data layer tells the truth.

02The render layer lies, the data layer does not

Ask the same application the same question as two different users and watch where the signal actually is. The page shell answers 200 either way; the API response body is where the boundary either holds or breaks:

// attacker authenticated as user A, owns order 1001 GET /orders/1002 page shell ──► 200 OK same HTML for any id render layer: looks fine ✗ false positive GET /api/orders/1001 own object ──► 200 OK A's order, A's tenant baseline, expected ✓ no finding GET /api/orders/1002 swap the id ──► 200 OK returns B's order, B's tenant differential proof ✓ finding raised
Same 200 on every line. The breach is not the status code - it is that the third response carries another tenant's record. Only the body separates a non-issue from a confirmed cross-tenant read.

The contrast is the whole point. The render-layer request (the first line) is the false positive a surface scanner raises: a numeric id in the URL, a 200 back, therefore "possible IDOR." The data-layer request (the third line) is the real finding, because the response proved the boundary failed. We refuse to confuse the two.

03How apPosture proves it, safely

apPosture confirms BOLA at runtime, differentially, and read-only. Authenticated as user A, the engine first requests A's own object to establish a baseline of what a legitimate response looks like. Then it swaps a single benign value - the object id - and asks for an object that belongs to user B. It does not delete, write or transfer anything; it issues the same idempotent GET an ordinary client issues, with one number changed. The probe is a read, the marker is a non-secret id, and the blast radius is zero.

A finding is raised only when B's data actually comes back to A: a different owner, a different tenant, fields that belong to an account the caller has no relationship with. When that happens, the PoC request and the response that leaked are attached to the finding verbatim, so a reviewer reads the proof rather than an inference. If the server instead returns 401, 403, 404 or an empty object - the boundary holding - no finding is raised, no matter how inviting the numeric id looked.

This is the same discipline that takes apPosture to zero false positives on object-level authorization categories: a cross-tenant read is reported when, and only when, the data crossed the tenant. The id is reachable on every endpoint with a numeric parameter; the exploit is confirmed on far fewer, and only the confirmed set becomes a finding.

04In-band versus differential, reachable versus confirmed

Two distinctions carry the entire BOLA verdict. The first is how the proof arrives: in-band, where one response simply contains data it should not, versus differential, where the contrast between two responses is itself the evidence. The second is what the proof claims: that a vulnerable path is reachable, versus that the boundary was actually crossed. Collapsing either pair is how reports get inflated.

DistinctionWeaker sideWhat apPosture requires
How it provesIn-band alone: a single 200 with a record in it - which is also what a legitimate request returns.Differential: B's object returned to A while A's own request returned A's object. The delta is the violation, not the status code.
What it claimsReachable: the endpoint takes an object id, so an IDOR is "possible." True of almost every REST API.Confirmed: the swapped id returned another tenant's data. The boundary demonstrably failed, with request and response attached.
Resulting stateA long queue of "possible IDOR" entries a reviewer stops trusting at the first false positive.A short queue where every item survives an audit because the cross-tenant read is in the evidence.

Reachable is a fact about the attack surface; confirmed is a fact about the response. We keep them as separate states and escalate to confirmed only on the second. A numeric id is an invitation to test, never a finding on its own.

05Why this is the bar that earns trust

A scanner that flags every numeric id as possible IDOR generates work without generating certainty, and it is the fastest way to make a security team stop reading the queue. BOLA punishes that approach hardest, because numeric object ids are everywhere and almost all of them are correctly guarded. The signal is rare; the false positives, if you accept render-layer reasoning, are endless.

Evidence over heuristics costs us findings a louder scanner would happily report. We accept that trade on purpose. For the breach you cannot see in the code, the only honest report is the one where another tenant's data is sitting in the proof - benign to obtain, impossible to argue with, and ready for the developer to close.

Evidence over heuristicsReachable beats foundMonitor → Block: rolling out a gate without slowing teamsIaC-grounded threat modelingAir-gapped AppSec with no phone-homeOne platform vs three tools
See it on your own app →