cybervaultechGet the free sample
← All articles
Application security

IDOR vs BOLA: The API Authorization Flaw Explained

Understand the difference between IDOR and BOLA, how object-level authorization fails, and how to test and prevent it with practical examples.

IDOR and BOLA usually describe the same underlying mistake: an application accepts a reference to an object but fails to verify that the current user is allowed to perform the requested action on that object.

IDOR, or Insecure Direct Object Reference, is the older, broadly recognized term. BOLA, or Broken Object Level Authorization, is the API-focused term used by the OWASP API Security Top 10, where it remains API1:2023.[1]

The useful question is not which acronym wins. It is: Does the server authorize this user, this action, and this object on every request?

A simple example

User A opens an invoice:

GET /api/invoices/1841 HTTP/1.1
Host: lab.example.test
Authorization: Bearer USER_A_TOKEN

User B changes the identifier to 1841 while using B’s own valid token. If the server returns A’s invoice, authentication worked—B is a known user—but authorization failed.

The same flaw can affect changes and deletion:

PATCH /api/invoices/1841 HTTP/1.1
Content-Type: application/json
Authorization: Bearer USER_B_TOKEN

{"delivery_email":"user-b@example.test"}

Read, update, and delete operations may use different code paths. Test each permitted method separately.

Direct does not mean sequential

Developers sometimes “fix” IDOR by replacing integer 1841 with a UUID. Unpredictable identifiers reduce casual guessing, but do not enforce permission. IDs leak through URLs, browser history, logs, analytics, emails, referrers, shared links, support tickets, mobile applications, and other endpoints.

OWASP explicitly recommends random identifiers only as defense in depth. Every function that uses a client-supplied identifier to access a record still needs an authorization check.[1:1]

Where object identifiers hide

Look beyond URL paths:

  • query parameters: /download?file=1841;
  • JSON bodies: {"account_id":"1841"};
  • GraphQL variables;
  • form fields and hidden inputs;
  • custom headers;
  • filenames and storage keys;
  • batch arrays;
  • nested child resources;
  • WebSocket messages.

An API may correctly protect GET /projects/{id} while exposing /projects/{id}/export, /files/download?project=id, or a GraphQL node lookup.

BOLA vs BFLA

BOLA concerns which object a user can access through a function they are otherwise allowed to use.

BFLA—Broken Function Level Authorization—concerns which function the user may call. A regular user invoking /admin/create-user is a function-level failure. A regular user invoking the legitimate /invoices/{id} function against another customer’s invoice is an object-level failure.

Both can appear together. A bulk administrative endpoint might expose an unauthorized function and fail to validate each object inside the request.

A controlled testing method

Only test applications you own or have written permission to assess.

1. Create a role-and-object matrix

Actor Own object Other user’s object Admin object Expected result
Anonymous no no no deny
User A yes no no mixed
User B yes no no mixed
Admin policy policy yes policy-based

2. Create controlled objects

Use two test accounts and distinctive synthetic values. This makes ownership clear without touching real customer data.

3. Capture the legitimate request

Record User A’s normal request through browser developer tools or an intercepting proxy. Then obtain User B’s normal request for the same function.

4. Change one variable

Replay the request using B’s valid session and A’s object identifier. Keep method, content type, and other values stable.

5. Evaluate the whole response

Status code alone is insufficient. A 200 may contain an error object; a 404 may contain sensitive metadata; response time or size may reveal whether an object exists.

6. Stop with minimal proof

One synthetic cross-user record is usually enough. Do not enumerate real objects or download unnecessary data.

A subtle multi-tenant failure

Consider:

GET /api/organizations/blue-team/reports/quarterly

A service may verify that the user belongs to an organization but fail to bind the report query to the authenticated organization. Changing blue-team to red-team could cross tenant boundaries.

The secure query should incorporate authorization context:

SELECT * FROM reports
WHERE report_slug = :slug
  AND organization_id = :authorized_org_id;

The server derives authorized_org_id from a trusted session and policy decision, not from a client-controlled field.

Why common fixes fail

“The front end hides the button”

The client is controlled by the user. Server-side authorization is mandatory.

“The user ID in the token matches the user ID parameter”

Ownership may involve organizations, delegations, shared objects, support roles, and policy exceptions. A simple equality check covers only a subset of real authorization models.

“We return 404 instead of 403”

Hiding existence can be sensible, but a different status code does not create authorization. The data-access decision must deny the record.

“The framework handles authentication”

Authentication identifies the caller. The application still needs an object-level policy.

Prevention architecture

Effective prevention combines:

  • deny-by-default authorization;
  • a centralized policy or consistently enforced service layer;
  • queries scoped to the authorized principal or tenant;
  • checks for every read and write, including batch actions;
  • unguessable identifiers as defense in depth;
  • automated cross-user, cross-role, and cross-tenant tests;
  • logs for denied and unusual object access;
  • careful handling of caches and background jobs.

A useful test pattern is:

def test_user_cannot_read_another_users_invoice(client, user_b, invoice_a):
    client.login(user_b)
    response = client.get(f"/api/invoices/{invoice_a.id}")
    assert response.status_code in (403, 404)
    assert invoice_a.customer_name not in response.text

Also test that the owner still succeeds. Security fixes that break legitimate access are incomplete.

Reporting the finding

Avoid a vague title such as “IDOR exists.” Write the outcome:

Authenticated customers can download other customers’ invoices by changing the invoice identifier.

Include the affected role, function, object, evidence, realistic acquisition of an identifier, business impact, and exact policy that should be enforced.

Sources


  1. OWASP Foundation, API1:2023 Broken Object Level Authorization. ↩︎ ↩︎

KEEP FOLLOWING THE THREAD

More from the notebook.

All articles ↗
Application security

OWASP Top 10:2025 Explained—What Changed and What to Fix First

A practical explanation of every OWASP Top 10:2025 category, the major changes from 2021, and how teams should use the list.

Read article
Identity security

Active Directory Attacks Explained: Four Identity Paths Defenders Should Understand

Understand Kerberoasting, AS-REP roasting, pass-the-hash, and NTLM relay—what each technique abuses, how they differ, and which defenses matter.

Read article
Emerging technology

Agentic AI Security: The New Risks Behind Tools, Memory, and Autonomous Action

Understand the OWASP Top 10 risks for agentic applications, from goal hijacking and tool misuse to memory poisoning, cascading failures, and rogue agents.

Read article