Case study

BOLA in the real world: paywalled APIs.

OWASP API Security Top 10 #1 — Broken Object Level Authorisation — is the most common API vulnerability and the hardest to catch with pattern-matching scanners. This is a real example of how it surfaces, and what the fix looks like.

What BOLA actually is

Broken Object Level Authorisation (BOLA) — also known as Insecure Direct Object Reference (IDOR) in older OWASP terminology — is a class of authorisation flaw where an API endpoint grants access to a resource based on an identifier in the request, without verifying that the requesting user has permission to access that specific resource.

The mechanism is simple: User A has a valid token. User A knows (or can enumerate) the identifier for User B’s resource. The API endpoint checks that User A has a valid token but does not check that User A owns or is permitted to access the resource with that identifier. User A accesses User B’s data.

BOLA is the top OWASP API vulnerability because it is invisible to most automated scanners. A pattern-matching scanner looks for SQL injection patterns, cross-site scripting payloads, and known vulnerability signatures. BOLA does not produce an error or an unexpected response pattern — it produces a 200 OK with data that the requesting user should not have received. Only a scanner that understands the authorisation model of the specific application can detect it.

The case: paywall bypass in 31 minutes

All details below are anonymised. The customer was a B2B SaaS company (sector withheld) that had been running an annual pentest with a human firm for three years. No BOLA findings had been reported.

T+0:00

Scan initiated

AssurePort Web Pentest started with scope limited to production API surface. DCV verified, RoE signed.

T+4:12

Recon complete

47 API endpoints discovered. Tech stack: Node.js/Express, PostgreSQL, JWT auth. Paywall tier structure inferred from response schema differences between free and paid endpoints.

T+11:38

Auth analysis

JWT algorithm: RS256, no algorithm confusion. Sessions: stateless. Paywall enforcement: client-side tier check in frontend, separate API gate per endpoint. Gap identified: /api/reports/{id}/status endpoint missing tier check.

T+19:44

Exploit attempt

Free-tier token presented to /api/reports/{id}/status with a report ID belonging to a paid-tier account. Response: HTTP 200, full report content including premium fields. PoC recorded.

T+24:51

Validation

Second attempt with a different free-tier token and a different paid-tier report ID. Confirmed reproducible. Severity: HIGH (CVSS 3.1: 8.1).

T+31:02

Report delivered

Finding report available in dashboard with PoC curl command, CVSS score, and remediation recommendation. Customer notified by email.

The proof-of-concept

The PoC that arrived in the customer’s dashboard was a single curl command that any developer could reproduce in their terminal:

# Free-tier token (legitimately obtained)
TOKEN="eyJhbGciOiJSUzI1NiJ9.eyJ0aWVyIjoiZnJlZSIsInN1YiI6InVzZXJfMTIzIn0..."

# Report ID belonging to a paid-tier account (discovered via predictable ID enumeration)
REPORT_ID="rep_98765"

# BOLA exploit: free-tier token accessing paid-tier report
curl -s -H "Authorization: Bearer $TOKEN" \
  https://api.[redacted].com/api/reports/$REPORT_ID/status | jq .

# Response: HTTP 200 with full paid-tier report content, including [redacted] premium fields

Why the annual pentest missed this: The human firm ran their annual test against a staging environment with synthetic data. The BOLA was only exploitable because the report IDs in production were sequential integers — a predictable pattern that the staging environment, which used UUIDs, did not reproduce. AI testing ran against the actual production surface.

The three-line fix

The remediation agent identified the specific Express route handler and generated the fix:

// Before: missing object-level authorisation check
router.get('/api/reports/:id/status', authenticate, async (req, res) => {
  const report = await db.reports.findById(req.params.id);
  return res.json(report);
});

// After: verify ownership before returning data
router.get('/api/reports/:id/status', authenticate, async (req, res) => {
  const report = await db.reports.findById(req.params.id);
  if (!report || report.userId !== req.user.id) {          // line 1
    return res.status(403).json({ error: 'Forbidden' });   // line 2
  }                                                         // line 3
  return res.json(report);
});

The customer deployed the fix within 24 hours of receiving the report. They also ran a secondary check across all 47 endpoints to identify any other instances of the same pattern — one additional endpoint was found and fixed in the same deploy.

The IDOR pattern across API designs

The paywall bypass described above is a specific variant of a broader IDOR/BOLA pattern. Common variants we encounter across web and API scans:

  • Sequential integer IDs on resources that should be private (report IDs, invoice IDs, user profile IDs)
  • Tenant isolation failures in multi-tenant SaaS — one tenant’s token accessing another tenant’s data
  • Tier bypass via indirect endpoints — a premium endpoint is gated, but a utility endpoint that returns the same data is not
  • File download IDOR — authenticated file download endpoints that check auth but not ownership
  • Webhook IDOR — webhook payload endpoints that accept any authenticated request regardless of target resource ownership

Pattern-matching scanners miss all of these variants because they do not produce exploit signatures. They look like legitimate authenticated requests. Only a scanner that models the authorisation structure of the application — building a map of which users should have access to which resources — can systematically test for them. That is what the AI pentest pipeline does in the auth analysis phase.