BOLA and IDOR in B2B SaaS APIs: The Vulnerability Class That Appears in Almost Every Pentest

If you run a B2B SaaS product and you've had an API pentest done, there's a good chance the report included at least one BOLA or IDOR finding. Not because your team is careless — but because this vulnerability class emerges naturally from how SaaS APIs are built, and it requires deliberate design choices to prevent.

BOLAIDORAPI SecurityAuthorizationSaaS

What BOLA and IDOR actually mean

IDOR stands for Insecure Direct Object Reference — the older OWASP term for when an API exposes a direct reference to an internal object (a database row, a file, a user record) and fails to verify that the requesting user is authorized to access it. BOLA — Broken Object Level Authorization — is the current OWASP API Security Top 10 term for the same class of vulnerability, framed more precisely around the missing authorization check rather than the exposed reference itself.

The practical definition: a user can access or modify another user's resources by substituting a different object identifier in an API request. You're authenticated — the API knows who you are — but it doesn't verify that the object you're requesting actually belongs to you. The distinction matters because BOLA isn't about unauthenticated access. It's about authorization logic that stops at the session level and never reaches the data level.

Why it's so common in SaaS APIs

The root cause isn't developer negligence. It's an architectural pattern that's extremely natural to fall into when building a SaaS product under normal delivery pressure.

Most SaaS APIs expose object identifiers directly in URLs and request bodies — /api/v1/reports/8821, /api/contracts/cc-0042, invoice_id: 5509. This is RESTful convention and completely reasonable. The problem emerges one layer up, in how authorization is implemented.

The most common pattern is what you might call HTTP-layer authorization: middleware checks that a valid session token exists, confirms the user is logged in, and passes the request through. That check happens once, at the route level. What it doesn't do is verify that the specific object being requested belongs to the authenticated user's tenant or account.

This gap is easy to introduce because the two concerns are handled at different points in the stack. Authentication middleware is a cross-cutting concern — it sits at the router level and runs before your controller logic. Object ownership is a data-level concern — it requires a database query that joins the requested record against the current user's context. When teams are moving fast, it's common to implement the first and assume the second is covered, or to implement it for some routes and miss others as the codebase grows.

Multi-tenant architecture compounds this. In a B2B SaaS product, tenant isolation means not just that User A can't read User B's data, but that Organization X can't read Organization Y's data even when both have valid authenticated sessions. Enforcing this consistently across hundreds of API routes, across multiple developers over multiple years, is genuinely hard to do without architectural guardrails.

How it manifests in real engagements

The specific patterns vary, but these are the scenarios that come up most often in B2B SaaS pentests.

Cross-tenant read via sequential IDs. A CRM platform assigned integer IDs to contact records — /api/contacts/1, /api/contacts/2, and so on globally across all tenants. An authenticated user in Tenant A could retrieve contact records belonging to Tenant B by incrementing the ID. The middleware verified the session; the handler fetched by ID without checking the contact's organization_idagainst the current user's organization.

Document access via UUID guessing isn't the fix.A document management product had switched from integer IDs to UUIDs, which the team believed prevented enumeration. During testing, the tester obtained a document UUID through the application's normal sharing workflow, then used it from a second account in a different organization. The UUID was non-guessable, but the access control check was still missing — knowing the UUID was sufficient to retrieve the document regardless of who owned it.

Write operations more severe than reads.A project management tool had partial BOLA coverage — GET endpoints on most resources checked ownership, but PATCH and DELETE handlers on the same routes didn't. An attacker who could obtain a valid resource ID (through a separate information disclosure issue, or simply through the application's own UI) could modify or delete records belonging to other tenants even though they couldn't directly read them.

Nested resource references. An invoicing API allowed fetching line items at /api/invoices/{invoice_id}/line-items/{item_id}. The invoice-level authorization was correctly implemented, but the line item handler fetched by item_idalone without verifying the line item belonged to the specified invoice. A tester could substitute a line item ID from a different organization's invoice and retrieve it successfully.

How a pentester finds these issues

The testing methodology is systematic and manual. Automated scanners don't reliably find BOLA because it requires business logic context — you need to understand what objects exist, which accounts own them, and what constitutes a cross-boundary access.

The standard approach: provision two accounts in separate organizations (or separate permission tiers within an organization), authenticate both through a proxy, and capture all API traffic from Account A during normal application use. That gives you a full list of object-referencing requests — every URL parameter, every JSON body field, every query string value that resolves to a database record.

Then replay each of those requests authenticated as Account B. For each one, check whether the response returns Account A's data, returns an error that reveals information, or accepts a modification. Cross-reference the results against Account B's own object IDs to confirm true cross-tenant access rather than coincidental overlap.

Beyond that baseline, good testers probe the less obvious surfaces: pagination parameters that return other tenants' records, filter parameters that bypass ownership scope, export and report generation endpoints that aggregate data without tenant filtering, and webhook or notification configuration endpoints where the target is a resource ID.

How to fix it — and where the fix needs to live

The right fix isn't a checklist applied retroactively to individual endpoints. It's an architectural pattern applied at the query layer so that authorization is structurally enforced rather than something individual developers remember to add.

Scope queries to the authenticated tenant at the ORM level. The most reliable approach is to make it structurally impossible to retrieve a record without the tenant scope applied. In practice this means your base query builder or repository layer always includes a WHERE organization_id = :current_org clause, and handlers call scoped query methods rather than bare findByIdcalls. A record that doesn't belong to the current tenant returns a 404 — the handler never sees it.

Ownership verification as a shared utility, not per-handler logic. If scoping at the ORM layer isn't immediately feasible across your codebase, the next best approach is a centralized assertOwnership(resourceType, resourceId, currentUser) function that handlers call before operating on a resource. The key is centralization — one function, fully tested, rather than ownership checks written differently in each handler.

Opaque IDs reduce enumeration exposure but don't replace authorization.Switching from sequential integers to UUIDs or other non-guessable identifiers raises the bar for enumeration attacks. It's a useful defense-in-depth measure. But it's not a substitute for the authorization check — as the example above shows, an attacker who obtains a valid ID through any channel can still exploit missing authorization logic.

Role-based middleware is necessary but not sufficient.Middleware that enforces role requirements (admin-only routes, subscription-tier gating) addresses function-level authorization. It doesn't address object-level authorization. Both need to be present. A common mistake is implementing robust role middleware and assuming that covers the authorization requirement.

Automated testing in the CI pipeline.Once the pattern is established, write tests that exercise cross-tenant access explicitly: Account B attempts to read, modify, and delete Account A's objects. These tests should fail with 403 or 404 responses. Running them in CI catches regressions before they reach production — particularly important as new routes are added.

Audit existing endpoints systematically.For a codebase that's been evolving for some time, a practical remediation approach is to enumerate all routes, identify which ones accept object reference parameters, and audit each one against a standard ownership verification pattern. Tools that can generate this route list from your framework's router configuration (Rails routes, Express route tables, Spring endpoint mappings) help scope the work.

Think your API authorization is solid?

BOLA appears in nearly every pentest we run. Book a scoping call and we will tell you what to test first.

Provecore Security

Exploitation-led web and API penetration testing for B2B SaaS teams. Every finding is proven exploitable with a working proof of concept.