Technical deep dive

Broken object level authorization: from invoice IDs to AI agents

What real-world disclosed reports say about the half of BOLA almost nobody tests.

You probably know the OWASP line by now: BOLA is a broken authorization check on a specific object. What that definition does not tell you is where it breaks in production, or how. That is the gap we are going to close.

Almost every team that tests for Broken Object Level Authorization tests the wrong half of it. The reflex, drilled in by a decade of guidance, is to ask a single question: can Account B read Account A's data? That question is not wrong. It is incomplete, and the gap it leaves is larger than most practitioners suspect.

When you study real disclosures rather than reason from first principles, unauthorized reads turn out to be about half the picture IRL. The other half is state change: modify, delete, approve, transfer, trigger. Close to half of the observed attack surface consists of an attacker acting on someone else's object rather than merely looking at it. A test suite that only replays GET requests across two accounts will, by construction, never see that half.

The findings here come out of a structured reading of 200 publicly disclosed HackerOne reports tagged IDOR or Improper Access Control between January 2021 and January 2026, filtered down to the confirmed in-scope BOLA cases and classified against a six-family taxonomy. What follows is an account of how this vulnerability actually manifests, which patterns recur, which identifier types are implicated, and the parts that should change how you develop and test tomorrow.

A note on the figures. This piece is deliberately qualitative. The exact proportions are still moving, although the shape of the distribution is stable.

Where you need precise figures, sample sizes, and inter-coder agreement, take them from the preprint and the open dataset rather than from prose. Both are linked at the end.

01The distinction that costs you

IDOR is not BOLA, and collapsing the two hides an entire attack class.

Practitioner language treats IDOR and BOLA as synonyms. They are not, and the difference is operational rather than pedantic.

Insecure Direct Object Reference, a term OWASP introduced back in 2007, names a mechanism where an attacker directly references an internal object through an exposed or predictable identifier, whether that is a URL parameter, a request body field, or a GraphQL node ID.

BOLA, the API security framing introduced in 2019, names a root cause: the server fails to verify that the caller is authorized for the specific object it is referencing.

BOLA is the broader concept. IDOR is one common road into it, and not the only one. You can reach a BOLA failure with no predictable reference at all, as in the chained-disclosure pattern, where the identifier is harvested from an earlier response rather than guessed.

Why does this alphabet soup matter? Because if your mental model is strictly an IDOR model, and you are looking only for enumerable or guessable references, you will never think to test the endpoint where the attacker already holds a legitimately obtained identifier and simply presents it somewhere it should not be honored. The reference looks clean. The authorization is still absent. That failure is invisible to an IDOR-shaped search, and it is a real family in the data.

So the terminology is addressed here deliberately. BOLA is the primary term, because the root cause is what you remediate. The identifier format is just the door the attacker happened to use.

02A measurement warning

Your IDOR tag count is lying to you.

Following that distinction, it is worth examining the quality of the source labels themselves, because the finding there is useful in its own right. A meaningful share of reports tagged IDOR or Improper Access Control on HackerOne do not, under rigorous criteria, describe BOLA at all.

The excluded material was not marginal. The leading exclusion class was an entirely wrong vulnerability type: cross-site scripting, server-side request forgery, plain CVEs, subdomain takeover. After that came pure authentication bypass with no object reference, business logic operating on the user's own resources, and reports too vague to classify. A large fraction of the sample fell out at this stage, and the residue that survived is what everything below is built on.

The practical consequence is direct. If you gauge your access-control exposure by counting tags, or by counting a scanner's raw IDOR hits, you are inflating the number, and worse, you are mis-targeting remediation effort. Read the loss rate conservatively and it still says the same thing: tag counting overstates the BOLA-specific signal by a wide margin.

On method, a language model was used strictly as a constrained extraction assistant populating a schema, never as the decision maker on family assignment, and every case was reviewed by a human. Independent re-coding of a stratified sample agreed on family assignment in the large majority of cases, with disagreements concentrated exactly where the taxonomy predicts ambiguity: at the boundary between a read that reveals another user's object and an action that changes it, and at the boundary between a user-to-user and an organization-to-organization crossing.

03BOLA is not one thing

Six ways object ownership actually breaks.

We identified six families, each defined by the primary structural or contextual condition that enables the authorization failure, not by endpoint semantics, HTTP method, or identifier format. A report belongs to the family that best explains why the check was absent or stale. They are ordered below by prevalence, heaviest first, so that the weight of the distribution is felt rather than merely stated.

Action-Level Object BOLA

The largest family in the corpus

The attacker performs an unauthorized state-changing action on another user's object: delete, modify, approve, transfer, trigger. Read-only access does not qualify. The signal is an action verb, the attacker doing something to an object that belongs to someone else.

Picture a project management app where you archive your own tasks through POST /tasks/{id}/archive. You substitute a colleague's task ID, and the server carries out the request. You were allowed to call that endpoint. It simply never checked whether you were authorized to perform that action on that task.

POST /tasks/42/archive · 42 belongs to a colleague Caller Session: user A Permitted to call this endpoint Server checks PASS Session is valid MISSING May act on task 42? Result Colleague's task is archived State changed, not read

Against Mozilla, an attacker sent the victim's email in the body of POST /v1/account/destroy using their own session. The server never verified the session-to-email binding, and it deleted the victim's account (report 3154983). Note the shape: function access granted, object-level authorization for a destructive action absent.

This is precisely the half that a read-only test protocol cannot reach. The OWASP 2023 revision covers it only partially, having added one deletion example, but the standard does not frame write, delete, and trigger operations across ownership boundaries as a dominant category. Given that this is the single largest observed family, that is a live blind spot. The guidance under-weights exactly what the data over-represents.

OWASP coveragePartial

Direct Object Reference

Close behind, and the classic that refuses to die

The attacker directly supplies or modifies an object identifier, whether a sequential integer, UUID, hash, encoded ID, email, or username, and reaches another user's object in a single request, with no prior acquisition step. The identifier was already known or trivially predictable.

Think of an invoicing app where your invoice sits at /invoices/1041. You change it to 1042, and your colleague's invoice loads. The server confirmed you were logged in. It never asked whether 1042 was yours.

GET /invoices/1042 · yours is 1041 Caller Session: user A Owns invoice 1041 Guesses 1042 Server checks PASS You are logged in MISSING Is 1042 yours? Result Colleague's invoice is returned One request, no setup

The disclosed example is a private data leak that reads almost quaint in its simplicity: GET /ocs/v2.php/apps/dav/api/v1/outOfOffice/{userId} on Nextcloud, where substituting another user's username returns their out-of-office record, travel destination and phone number included (report 3382343).

This is the paradigm case for OWASP's framing. The shop-name enumeration scenario and the preference for unpredictable GUIDs address it directly. It is also the case your team is most likely to already be testing.

OWASP coverageDirect

Tenant Isolation

Wrong company, same request. A distant third by volume, and the highest blast radius per incident

The attacker crosses an organizational, tenant, or workspace boundary. The violated line is organizational membership specifically, not merely user identity. If User A reads User B's data, that is Direct Object Reference. If Organization A reads Organization B's data, that is Tenant Isolation.

A SaaS platform scopes records under a company_id. Your session belongs to Company A. You drop Company B's identifier into a request. The server validates that your session is genuine, but never that it belongs to Company B.

POST /bugs.json · organization_id = ORG B Caller Member of ORG A No membership in ORG B Server checks PASS Session is authentic MISSING Do you belong to B? Result ORG B private reports returned One ID, every tenant

The sharpest disclosed instance landed on HackerOne itself: an attacker passed an arbitrary organization_id to POST /bugs.json and read private bug reports belonging to any organization, with no membership verification (report 2487889). The boundary fails at scale too. In May 2026, Cisco disclosed a maximum-severity flaw in the internal REST API layer of Secure Workload that allowed cross-tenant access across both its SaaS and on-premises deployments. The access-validation logic checked that a caller was authenticated, but not which organization's workload it was entitled to reach.

Conceptually this sits inside OWASP's object-level authorization model, but it is absent from the standard's examples, terminology, and prevention guidance. A reader working only from the standard would not know to register two organizations and test the boundary between them.

OWASP coverageAbsent

Workflow-Context

The auth check that went stale. Smaller by volume, and architecturally the most demanding

Authorization is bypassed specifically because of the object's state or lifecycle position: draft, archived, deactivated, deleted, transferred. The same request against a normal-state object would be rejected.

Consider a CRM. When a staff member leaves, their access is meant to be revoked, but the customer-records endpoint only checks whether the requester holds an active staff session, not whether their access to this specific customer was ever withdrawn. The session is still valid. The object-level revocation was never enforced.

GET /customers/77 · the same request at three moments T0 · GRANTED Staff of Company X Reads customer 77 PASS · LEGITIMATE T1 · STATE CHANGES Staff dissociated MISSING Grants re-evaluated? T2 · REPLAY Customer PII still returned Session still valid

Shopify disclosed this shape: a staff member continued to read a removed customer's personal information after dissociation from the company, where the removal state should have revoked access but did not (report 2855610).

The architectural point is what makes this family important out of proportion to its share of the corpus. A standard ownership check passes here, because under normal object state the requester genuinely is authorized. The failure is conditional on the lifecycle transition, which means it cannot be caught by static per-request ownership checks alone. It requires authorization to be re-evaluated when object state changes, which is a different engineering commitment from adding a predicate to a query.

OWASP guidance does not address authorization re-evaluation on deactivation, archival, or removal. Static ownership checks at access-grant time are insufficient for this family, and the standard does not acknowledge the gap.

OWASP coverageAbsent

Chained Disclosure

Two moves, one breach. Rarer, and systematically under-tested

Exploitation requires a distinct prior step to harvest the identifier from a separate endpoint. The attack reads as two acts. In the first, the attacker observes a response that leaks an identifier scoped to someone else's object. In the second, they present that identifier at a different endpoint that performs no cross-user ownership check. The authorization class is the absent check at the consuming endpoint. The identifier harvest is the precondition that makes the failure reachable, not the failure itself.

Act 1 · harvest Attacker Crafted bulk query Own session, own account Endpoint one Returns ContentDocument IDs for other users' uploaded files Identifiers you could not have guessed Act 2 · use Attacker Presents one harvested ID The reference is legitimate, not predicted Endpoint two PASS Document reference is valid MISSING Does it belong to you? Failure sits at endpoint two

The confirmed case is instructive in scale. Against a United States Department of Defense system, an attacker first sent a crafted payload to a Salesforce Aura API endpoint and received roughly two thousand ContentDocument IDs belonging to other users' uploaded files. Each ID, obtainable only through that initial bulk query, was then used individually against a second endpoint that validated the document reference but performed no ownership check against the requesting session (report 2623715).

OWASP's shop-revenue scenario is loosely two-step, but there is no guidance on identifier provenance, on cross-endpoint leakage as a precondition, or on multi-step authorization testing. The reader inherits the blind spot of treating each endpoint in isolation.

OWASP coverageAbsent

Object Rebinding

Just change the owner field. The rarest family, and the most expensive when it lands

The attacker modifies an ownership-identifying field in the request, such as owner_id, account_id, user_id, or msg.Sender, and the object changes ownership or authorization scope. The exploit targets the field that determines who owns the object, not merely which object to touch.

A collaboration app lets users update document metadata through PATCH /documents/{id}. The body includes an owner_id meant for internal use. The attacker replaces it with another user's identifier, and the server trusts the client-supplied ownership field.

PATCH /documents/42 · body { "owner_id": "victim" } Caller Session: user A Supplies owner_id in the request body Server checks PASS Session is valid MISSING Owner from session? Result Ownership scope is rewritten Client named the owner

The disclosed case sits in financial infrastructure, which is where trusting client-supplied ownership becomes expensive. In the Cosmos SDK, an attacker packed the victim's address into msg.Sender within a MsgExecute. The checkSender routine validated msg.Sender rather than the authenticated context, and funds moved out of the victim's account (report 2976481).

Client-supplied ownership-field manipulation is absent from OWASP examples and prevention guidance entirely, and it lacks a direct CWE equivalent. If you have never been told to distrust owner_id in a request body, this is why.

OWASP coverageAbsent

Coverage at a glance

FamilyRelative weightOWASP API Top 10 coverage
Action-Level ObjectLargest familyPartial: one deletion example, no framing as a category
Direct Object ReferenceClose secondDirect: the paradigm case in the standard
Tenant IsolationThird, high blast radiusAbsent from examples and prevention guidance
Workflow-ContextSmall, architecturally hardestAbsent: no re-evaluation on state change
Chained DisclosureRare, under-testedAbsent: no identifier provenance guidance
Object RebindingRarest, highest costAbsent

What the standard covers well is what most teams already implement. What it covers poorly or not at all is where a large share of confirmed breaches now sit.

04Two things that change how you see BOLA

Two dimensions cut across all six families.

BOLA climbs, it does not only move sideways

Authorization failures run in two directions. Horizontal is the familiar one: a user reaches an object belonging to a peer at the same privilege level. Vertical is the one the playbooks forget: a lower-privileged user reaches an object owned by a higher-privileged role, typically an administrator.

Horizontal failures dominate the corpus. Vertical failures are a clear minority, but a persistent one, and they are more dangerous than their share suggests.

The crucial clarification is that vertical BOLA is not a role-gating failure, and not the same thing as Broken Function Level Authorization. In every vertical case in the dataset, the attacker's role was legitimately permitted to call the endpoint. The missing control was ownership of the specific object, not access to the function.

A standard Nextcloud user deleted an administrator's external storage configuration (report 2212627). A Frontegg admin modified an Owner's API key through an undocumented PATCH method (report 2149124). Multiple Autodesk users gained modification capability over admin-owned objects.

In each, a lower-privileged session reached up and acted on something above it, gaining effective administrative reach without ever being granted an administrative role. That is why vertical cases carry disproportionately high severity, and why their absence from most testing playbooks is a genuine visibility gap.

Opacity narrows the door. It does not close it.

UUIDs do not save you

There is a comfortable belief that swapping sequential integers for opaque identifiers closes BOLA. The data does not support it.

Sequential integers do lead among the cases where the identifier format could be determined, which confirms that predictable IDs remain the highest-risk design choice. But non-sequential formats, meaning encoded IDs, usernames, emails, UUIDs, and hashes, account for a substantial minority of those cases.

The cleanest illustration is the GraphQL Global ID. A GID is a base64-encoded node identifier that typically encodes a service name, an object type, and a numeric backend ID, and it looks something like gid://service/Type/N. Because that backend integer is sequential, an attacker who decodes one GID can increment the integer, re-encode, and reference a different user's object, walking straight through the apparent opacity. This decode, increment, re-encode pattern was exploited against HackerOne (report 2633771) and several others.

The lesson for API design is narrow and firm: do not treat opacity of encoding as a replacement for access control.

05Same old BOLA, with a new agent in the middle

When an old vulnerability wears an LLM costume

Every case above involves a request the attacker sent themselves, authenticated as themselves. That assumption broke in public over the weekend of 31 May 2026.

Meta had put an AI assistant in front of Instagram account recovery. Attackers opened a support conversation, gave the assistant a target username and an email address they controlled, and asked it to attach that address to the account. It did. The one-time code went to the attacker's inbox, the reset link followed, and the owner was locked out. Routing the session through a VPN near the target's usual location was enough to satisfy the location signal the assistant was using. The archived Obama White House account, the account of the Chief Master Sergeant of the United States Space Force, and Sephora's brand account were among those taken over, some of them defaced. In its breach notification to the Maine Attorney General, Meta put the number at 20,225 accounts and the window at 17 April to 31 May 2026, when it disabled the tool and invalidated the reset links it had produced. Earlier reporting claims activity going back to February, which Meta's stated window does not cover, so treat the true duration as contested. Krebs reported that the technique failed against accounts with any form of multi-factor authentication enabled, including SMS. Meta said on 1 June that the issue was fixed. Takeovers were still being reported on 2 June. Sources: KrebsOnSecurity, Help Net Security, 404 Media.

It has been filed almost everywhere as a prompt injection story. Read it as an authorization failure instead and the shape is one you have already met twice in this piece. The assistant was permitted to change a recovery address, which is function-level access, and it never established that the person asking controlled the account it was asked to change, which is the object-level check. It derived ownership from what the requester supplied, a username and an IP that a VPN can move. That is Action-Level Object BOLA, family one, performed by a deputy. It is also Object Rebinding, family six, because the field being written is the one that determines who owns the account.

SUPPORT CHAT · target username + attacker's email Attacker Holds no session on the account VPN near the owner's location Support agent Permitted to change recovery email. Believes what it is told. THE DEPUTY HAS THE PRIVILEGE. THE ATTACKER JUST ASKS. Account recovery Platform checks PASS Request formed, plausible MISSING Do you control this account? Result Recovery email rebound, code sent to the attacker, owner locked out Ownership taken from a claim

The label matters here in exactly the way section 02 described. Press coverage calls it prompt injection. Vendors, in the cases below, call it working as intended. Neither phrase contains the words missing object-level authorization, which is what would have to be written down for anyone to count these later.

The other public cases divide by how the instruction arrives. Above, the attacker spoke to the agent directly. In the indirect variant the attacker plants text where an agent will read it. In November 2025 Aaron Costello of AppOmni showed the same thing between agents. ServiceNow Now Assist agents ship grouped into one team and discoverable by default, so one can recruit another. A low-privileged user who could not read incident INC0000001 under the access controls planted a prompt, and an agent copied the sensitive contents into INC0000002, a ticket that user did own. Other scenarios induced an agent to assign the admin role to the attacking user. Agents run at the privilege of whoever started the interaction, not whoever wrote the prompt. ServiceNow's position after disclosure was that the behaviour is intended, with documentation updated to describe the risk (AppOmni Labs). Action-Level and the vertical direction in one attack, with no object-level check anywhere in the agent's path.

A lot of AI products still use ordinary APIs. In July 2025 Meta fixed a flaw reported by Sandeep Hodkasia of AppSecure where guessable identifiers on edited prompts let any logged-in user retrieve another user's prompts and generated media, traced to a missing authorization check on a media_set_id in a GraphQL mutation (TechCrunch). This is basically Direct Object Reference wearing an LLM costume.

FamilyHow it reappears at the agent boundaryPublic case
Action-Level ObjectA deputy with write authority acts on an object for someone who does not own itMeta support assistant, May 2026
Object RebindingThe ownership field itself is written from a claim the requester suppliedMeta support assistant, May 2026
Vertical directionThe agent runs at the initiating user's privilege and can be induced to grant rolesServiceNow Now Assist, Nov 2025
Chained DisclosureInstruction planted on a public surface, acted on with a token that also spans a private oneGitHub MCP, May 2025
Tenant Isolation, in spiritUntrusted external content enters the internal context and leaves with its contentsEchoLeak, June 2025
Direct Object ReferenceAn AI product API with guessable object IDs and no ownership checkMeta AI prompts, July 2025

Notice what these have in common. In none of them was a check missing in the way a scanner would recognise. The token was valid, the scope was as configured, the support agent was doing the job it was given. What is absent is an authorization decision at the moment a deputy acts on a specific object, and any notion of who is actually asking. That is the gap from section 01 moved one layer up, and the identifier-shaped mental model fails here even harder than it fails on plain APIs, because there is often no identifier to fuzz at all.

This is also where the evidence is new and emerging. Public agentic cases are counted in dozens rather than hundreds, several were closed by vendors as intended behaviour rather than as vulnerabilities, and there is no tag to sample from the way there is for IDOR. The measurement problem returns in a harder form, because the label does not exist yet. That is an argument for running the six families against agent surfaces deliberately, now, rather than waiting for disclosures to arrive tagged and countable.

06What correct testing actually has to do

Six requirements that follow from where the failures are.

Put the distribution and the two cross-cutting dimensions together, and the shape of an adequate BOLA test suite is no longer a matter of taste. It is dictated by where the confirmed failures actually are, on plain APIs and on agent surfaces alike.

Exercise every HTTP verb, not just GET.

The Action-Level family is the single largest, and a read-only protocol misses it by construction. Delete, modify, and trigger operations across ownership boundaries are first-class test cases, not edge cases.

Provision two accounts with distinct object ownership before you begin.

Single-account testing cannot confirm a cross-user boundary failure. There is no way around this. The boundary is the thing under test.

Walk objects through every lifecycle state.

Active, archived, deactivated, deleted. Workflow-Context failures are conditional on state transitions and are invisible to a single-state test flow.

Map identifier-returning endpoints first.

Any endpoint that returns another user's object identifier is a potential chained-disclosure source. You cannot test the second act if you have not catalogued the first.

Test tenant boundaries for any multi-tenant application.

Register two separate organizations and verify that requests from one cannot reach the other's objects.

Include the vertical direction.

Test whether a lower-privileged session can act on objects owned by higher-privileged roles. Most playbooks never look.

Test the agent as a caller.

If any agent, assistant, or MCP server holds a credential on a user's behalf, treat it as an account in the matrix above. Ask it directly, and plant an instruction in content it will read, then see which objects it will touch on someone else's behalf.

Fuzzing an ID parameter finds the sequential-integer cases, which are real and worth finding, but they are a modest slice of the population. The rest demand semantic understanding of which parameters are object references, whether responses differ across users, and whether that difference constitutes a boundary violation. That is the design premise behind how APIsec tests for BOLA: rather than merely fuzzing identifiers, the platform generates custom scenarios from two accounts with distinct ownership.

07Fixes you can hand your team today

Authorization is a server-side fact, not a client-supplied claim.

The remediation guidance is shorter than the taxonomy, because most of the families reduce to that one principle applied in different places.

Derive object authorization from the authenticated session, server-side, on every request.

Never read ownership from the request body or a query parameter. That single habit closes the Object Rebinding family outright.

Apply ownership scoping at the database query layer.

The query itself should carry both the object ID and the authorization predicate, rather than filtering in application code after retrieval, which is where the check gets forgotten.

Re-evaluate authorization when object state changes.

Deactivation, archival, and removal must invalidate existing access grants to that object. This is the only thing that closes Workflow-Context.

Treat every identifier format as potentially enumerable.

UUIDs and encoded IDs reduce predictability, but they do not eliminate risk when an identifier leaks elsewhere in the API.

Check GraphQL Global IDs at the resolver, not the schema.

If your GIDs encode sequential backend integers, the schema's type system will happily hand back an object the resolver never checked you owned.

08A reality check: Where this leaves us

The gap is not knowledge.

Across the dated subset of our research corpus, BOLA shows no meaningful decline over the 2023 to 2026 window, a window that spans the OWASP API Security Top 10 2023 update, with awareness of this vulnerability arguably at its highest. If knowledge alone closed the gap, the curve would bend. It does not appear to.

That locates the real gap in remediation and in testing discipline, not in whether developers have heard of BOLA. They have. The bugs persist anyway.

A candid limit on our research is that the evidence base is one platform, one snapshot, and it tilts toward developer-tooling companies that run public bounty programs and choose to disclose. Fintech, healthcare, and enterprise-internal APIs are almost certainly under-represented relative to their true exposure. Exactly one confirmed fintech case appears in the corpus, which reflects conservative disclosure norms far more than it reflects low vulnerability density.

So the distribution here is a floor on the diversity of BOLA in the wild, not a census. If anything, that is a reason to look harder at your own less-visible surface, not less. The vulnerability families you do not see disclosed are not the families you do not have.

Before you close this tab

Every family in this piece has existed since the first API shipped an object ID. What changed in May was who's allowed to ask for one. The check that was missing on Instagram's support bot is the same check that was missing on Mozilla's account-deletion endpoint in 2021. The same gap is going to have faster messengers. Are your applications ready? See for yourself using apisec.