Blog · C.06 · API Security Deep-Dive

BOLA in REST APIs: why scanners miss the most common API1 finding

Broken Object-Level Authorization is OWASP API Security Top 10 #1 for a reason - and automated scanners are structurally incapable of finding it. Here's the manual testing methodology, how it applies to GraphQL, compound key patterns, and the one architectural fix that actually works.

BOLAIDOROWASP API Top 10RESTGraphQLAPI Pen Test
API1: BOLA · Object-Level Auth · Horizontal Escalation · Vertical Escalation · UUID Enumeration · Sequential ID · Compound Key · GraphQL BOLA · Tenant Boundary · Missing Ownership Check API1: BOLA · Object-Level Auth · Horizontal Escalation · Vertical Escalation · UUID Enumeration · Sequential ID · Compound Key · GraphQL BOLA · Tenant Boundary · Missing Ownership Check
// TL;DR

BOLA (Broken Object-Level Authorization) - also known as IDOR - occurs when an API endpoint returns or mutates a resource based on an identifier in the request without verifying that the authenticated caller is authorized to access that specific object. Scanners can't find it because they don't know which objects belong to which user. Only a tester with two separate user accounts can verify whether Account A can access Account B's resources. We find it in over half of the APIs we test. Here's exactly how.

// 01 What BOLA is and why it's API1

An API endpoint that accepts an object identifier - a user ID, document ID, order ID, message thread ID - is implicitly making a promise: "I will only return or modify this object if the caller is authorized to interact with it." BOLA is what happens when that promise is broken: the authorization check is absent, performed after the data is already fetched, checked against the wrong identity context, or enforced only on some endpoints and not others.

OWASP placed it at API1 in their API Security Top 10 (2019 and 2023 editions) because it's the highest-frequency high-severity API vulnerability class. It appears consistently across REST APIs, GraphQL APIs, and gRPC services regardless of the language, framework, or cloud platform. It's also the hardest class to find with automation - which is why it persists at #1 while developers are improving injection and auth failure rates every year.

#1OWASP API Security Top 10 position - held in both 2019 and 2023 editions
>50%Of APIs we test have at least one exploitable BOLA finding
0%Detection rate for BOLA in generic DAST scanners without context
CriticalCommon severity - cross-tenant BOLA exposes every customer's data

// 02 Why scanners are structurally incapable of finding BOLA

This isn't a limitation of scanner quality - it's structural. To detect BOLA, you need to answer the question: "Can User A access User B's resources?" That question requires:

No automated scanner has two user accounts with relationship-mapped resource ownership built into it. Generic scanners that attempt BOLA detection by fuzzing IDs produce enormous false-positive rates because they don't know what a legitimate "not found" response looks like vs. an unauthorized access that returns 200.

"The scanner's score tells you about your known vulnerability database coverage. The pen tester's report tells you whether a competitor can read your customer's data. These are not the same question."

CyberFortify API security practice

// 03 The three types of BOLA in production APIs

Type 1 - Sequential or predictable IDs

The simplest and most reliably exploitable pattern. Resources are identified by sequential integers or predictable short strings. User A creates a document and receives ID 4821. They can enumerate downward and access documents 4820, 4819, etc. - all owned by other users.

HTTP# User A is authenticated. User B created invoice 9183.
GET /api/v1/invoices/9183
Authorization: Bearer <user-A-token>

HTTP/1.1 200 OK
{
  "id": 9183,
  "owner_email": "[email protected]",   # ← User B's data
  "amount": 142500,
  "line_items": [...]
}

# Severity: Critical if data is sensitive (PII, financial, PHI)
# CVSS: 8.1-9.8 depending on data classification

Type 2 - UUID-based IDs (still exploitable)

Many developers believe switching from sequential integers to UUIDs solves BOLA - it doesn't. UUIDs prevent guessing but not leakage. If a UUID appears in any server response, URL, email notification, webhook payload, or API log that User A can access, User A can use it to probe the API. The test is identical to Type 1 - the attack surface is narrower only if UUIDs are never exposed outside their authorized context.

HTTP# UUID leaked in a webhook notification payload to User A:
POST /hooks/user-a-endpoint
{ "event": "comment_added", "document_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }

# User A uses that UUID directly against the documents endpoint:
GET /api/v2/documents/f47ac10b-58cc-4372-a567-0e02b2c3d479
Authorization: Bearer <user-A-token>

HTTP/1.1 200 OK   # ← User B's document, fully accessible

Type 3 - Compound keys and nested resources

The most frequently missed pattern. Authorization is checked at the top level but not on nested resources. An API correctly verifies that User A can access /organizations/123, but doesn't re-verify on /organizations/123/members/456/private-notes - allowing horizontal access within a resource hierarchy that should be further restricted.

HTTP# Correct: User A is a member of org 123
GET /api/v3/organizations/123
Authorization: Bearer <user-A-token>
→ 200 OK (authorized)

# BOLA: member ID 456 belongs to User B, private notes are not shared
GET /api/v3/organizations/123/members/456/private-notes
Authorization: Bearer <user-A-token>
→ 200 OK ← authorization checked org membership only, not note ownership

// 04 The manual testing methodology

Our API BOLA testing methodology requires two accounts in the same application with distinct resource sets. We call these Account A (attacker) and Account B (victim). The test cycle for each resource type is:

Step 01

Map every ID-bearing endpoint

Proxy all authenticated traffic for Account B through Burp. Extract every endpoint that accepts a resource identifier - in path, query string, or request body. Use Burp's Logger++ or a custom match-and-replace to tag them.

Step 02

Replay with Account A token

For every captured request, swap the Authorization header to Account A's token and replay. Use Burp's "Match and Replace" rule or Autorize extension to automate token substitution across a full authenticated session.

Step 03

Interpret responses carefully

A 200 response with Account B's data is a confirmed BOLA. A 403/401 is a pass. A 404 may mean the check passed (resource hidden) or the ID was wrong - probe neighbouring IDs to distinguish. A 200 with empty data may indicate a silent access-control failure.

Step 04

Test write operations too

BOLA applies to mutations as well as reads. Test PUT, PATCH, DELETE on Account B's resources with Account A's token. Write-BOLA is often higher severity than read-BOLA because it allows data tampering or deletion across tenant boundaries.

Step 05

Check admin-only endpoints

Test whether a regular Account A token can access admin-level endpoints by guessing or enumerating admin resource IDs. Vertical BOLA (user → admin) is typically Critical severity.

Step 06

Document the ID source

For each BOLA finding, document where the victim ID was obtained - leaked in another response, guessable, enumerable. This determines exploitability and drives the CVSS score. Guessable IDs are always Critical; UUID-based IDs leaked through an ancillary channel are High.

// 05 GraphQL BOLA: a distinct challenge

GraphQL changes the BOLA test methodology significantly. Where REST has discrete endpoints per resource, GraphQL exposes a single endpoint with a queryable graph. Object-level authorization must be enforced at the resolver level - for every field, on every object type, in every query depth. Missing a single resolver allows BOLA through a deeply nested query.

GRAPHQL# Standard BOLA - User A queries User B's private document via direct ID
query {
  document(id: "uuid-of-user-b-doc") {
    title
    content      # sensitive field
    owner { email }
  }
}

# Nested resolver BOLA - authorization checked on 'project', not on 'invoices'
query {
  project(id: "user-a-project-id") {
    name
    team {
      members {
        invoices {        # ← resolver missing auth check
          amount
          lineItems { description }
        }
      }
    }
  }
}

# Batching BOLA - send multiple document IDs in one query
query {
  doc1: document(id: "user-b-doc-1") { content }
  doc2: document(id: "user-b-doc-2") { content }
  doc3: document(id: "user-b-doc-3") { content }
}

For GraphQL APIs, our test methodology adds two extra steps: introspection mapping to identify all query and mutation fields that accept ID arguments, and depth-first traversal of nested relationships to check that authorization is re-evaluated at each resolver level, not just at the query root.

// 06 The one architectural fix that actually works

Band-aid fixes - adding WHERE id = ? AND user_id = current_user() to individual database queries - work, but they require every developer to remember to apply them on every endpoint. The pattern that actually eliminates BOLA at scale is context-bound object retrieval: the data access layer never accepts a bare object ID. It always requires an authorization context, and that context is injected from the authenticated request, not supplied by the caller.

TYPESCRIPT// ❌ Vulnerable pattern - bare ID lookup, authorization forgotten
async function getInvoice(invoiceId: string) {
  return db.invoices.findById(invoiceId); // no ownership check
}

// ✅ Context-bound pattern - authorization is structurally enforced
async function getInvoice(invoiceId: string, ctx: AuthContext) {
  const invoice = await db.invoices.findOne({
    id: invoiceId,
    tenantId: ctx.tenantId,   // ← injected from JWT, not from request
    // Optional: also check user-level ownership within tenant
    ...(ctx.role !== 'admin' && { ownerId: ctx.userId })
  });
  if (!invoice) throw new NotFoundError(); // same error for missing & unauthorized
  return invoice;
}

// Key: NotFoundError instead of ForbiddenError prevents information leakage
// about which IDs exist outside the caller's authorization context
PatternPrevents guessingPrevents BOLANotes
Sequential integer IDsNoNoWorst case - trivially enumerable
UUID v4 IDsYesNoStill vulnerable if UUID leaks
Context-bound DB lookupN/AYesCorrect fix - authorization structural
Object-capability referencesYesYesBest practice - token IS the capability
CY

CyberFortify Research

API Security Practice

Field notes from REST, GraphQL and gRPC API penetration testing engagements aligned with OWASP API Security Top 10. All findings are drawn from real authorised assessments.

Have your APIs been tested for BOLA by a human?

Scanners report zero BOLA findings on most APIs - not because there are none, but because they can't look. CyberFortify's API pen testing service uses two separate authenticated accounts and systematic object-level testing on every resource endpoint.

Schedule scoping call → API pen testing →