Broken Object Level Authorization (BOLA): My Step-by-Step Testing Guide for API’s #1 Vulnerability

If I had to pick the single most common and most damaging API vulnerability I come across, it would be Broken Object Level Authorization, or BOLA. It sits at the top of the OWASP API Security Top 10 for good reason — it’s simple to understand, simple to test for, and shockingly common even in mature, well-funded applications. In this guide, I’ll explain exactly what BOLA is, why it happens so often, and the full methodology I use to find it.

What Is Broken Object Level Authorization?

BOLA happens when an API endpoint checks that a user is authenticated, but fails to check whether that specific user is actually authorized to access the specific object being requested. In plain terms: the API confirms “you’re logged in,” but never confirms “you’re allowed to see this particular record.”

The classic example looks like this:

GET /api/orders/1001

I’m logged in as a regular user, and this returns my order. Now I simply change the ID:

GET /api/orders/1002

If this returns someone else’s order — an order I never placed and have no relationship to — that’s BOLA. The API authenticated me just fine; it just never checked that order 1002 actually belongs to me.

You might also hear this called IDOR (Insecure Direct Object Reference) — BOLA is essentially the modern, API-focused evolution of that same underlying concept.

Why BOLA Happens So Often

From what I’ve seen testing dozens of APIs, BOLA keeps showing up because:

  1. Developers correctly implement authentication (checking “is this a valid logged-in user?”) but forget authorization (checking “does this user own this specific resource?”) are two completely separate concerns.
  2. Authorization checks are added to some endpoints but forgotten on others, especially newer or less-visible ones like export, admin, or reporting endpoints.
  3. Object IDs are often sequential integers or easily guessable UUIDs, making enumeration trivial once the authorization gap exists.
  4. Nested resources (/api/users/5/orders/1002) sometimes only validate the outer object, not the inner one.
  5. Different HTTP methods on the same resource path aren’t tested equally — a GET might be properly protected while a DELETE or PUT on the same object isn’t.

Step 1: Mapping Every Endpoint That References an Object

Before testing, I go through the entire API and list every endpoint that takes an identifier as a parameter — whether in the URL path, query string, or request body:

GET  /api/users/{id}
GET  /api/orders/{orderId}
PUT  /api/profile/{userId}
DELETE /api/documents/{docId}
POST /api/messages/{conversationId}/reply
GET  /api/invoices?accountId={accountId}

Any place an ID appears is a candidate for BOLA testing. I don’t skip anything, including endpoints that seem “low value” — attackers don’t skip them either.

Step 2: Creating Two Test Accounts

I always set up at least two accounts (User A and User B) with no special relationship to each other. This is non-negotiable for proper BOLA testing — testing with a single account or with admin credentials will hide these bugs completely.

Step 3: The Core BOLA Test

For every identified endpoint, I:

  1. Log in as User A and create or access a resource, noting its ID.
  2. Log in as User B (different session/token).
  3. Attempt to access User A’s resource ID using User B’s session.
  4. Check whether the response returns User A’s actual data, or is properly denied.

I test this across every HTTP method the endpoint supports:

GET    /api/orders/1001   (as User B) — can I view it?
PUT    /api/orders/1001   (as User B) — can I modify it?
DELETE /api/orders/1001   (as User B) — can I delete it?

It’s common to find that GET is protected but PUT or DELETE isn’t, because a developer remembered to secure the “view” logic but forgot the “edit” or “remove” logic.

Step 4: Testing Indirect Object References

Not every BOLA vulnerability is as obvious as an ID in the URL. I also test:

Step 5: Testing Nested and Relational Resources

APIs with nested resource structures need extra scrutiny:

GET /api/organizations/{orgId}/projects/{projectId}/tasks/{taskId}

I test:

I specifically test mismatched combinations — my own orgId paired with someone else’s projectId and taskId — to see which level of the hierarchy actually gets validated.

Step 6: Testing ID Enumeration and Predictability

Even where authorization checks exist, I check how guessable the IDs themselves are:

Step 7: Testing GraphQL-Specific BOLA

Since GraphQL uses a single endpoint, BOLA testing looks a little different — I test whether individual resolvers validate ownership on every object field, not just the top-level query:

query {
  order(id: "1002") {
    id
    total
    customer {
      email
      address
    }
  }
}

I specifically test nested object resolvers, since a common mistake is validating access on the top-level order field but not re-validating on the nested customer object it returns.

Step 8: Automating BOLA Discovery at Scale

For APIs with hundreds of endpoints, manual testing every combination isn’t practical, so I also:

Common Places I Keep Finding BOLA

  1. Export and reporting endpoints, which are often built later and get less security review than core CRUD endpoints.
  2. Admin panels that reuse the same API as the regular app but assume “no one will call this without the admin frontend.”
  3. File and document download endpoints using a direct file ID or path.
  4. Messaging/chat features where conversation IDs aren’t tied back to participant verification.
  5. “Duplicate” or “clone” features that let you reference another object’s ID as a template, which sometimes skips ownership checks since it’s technically a “create” operation.

How I Recommend Fixing BOLA

Final Thoughts

BOLA is proof that the most damaging vulnerabilities aren’t always the most technically complex ones. It’s usually just a missing WHERE clause or a forgotten if check — but the impact can mean any authenticated user reading, modifying, or deleting any other user’s data across the entire platform. Every time I test a new API, this is the first thing I check, and it’s rarely a wasted effort.

Exit mobile version