Understanding Broken Object Level Authorization (BOLA) in Modern REST APIs
Broken Object Level Authorization (BOLA/IDOR) remains the number one vulnerability in the OWASP API Security Top 10. This writeup examines root causes, detection patterns, and architectural defenses.
01.Overview & Architectural Context
BOLA occurs when an API endpoint accepts an object identifier from user input and retrieves or modifies the corresponding record without asserting whether the authenticated actor possesses permission to access that specific object. While simple in theory, in distributed microservices and GraphQL federations, object-level authorization is often mistakenly offloaded to edge proxies or neglected entirely.
02.Attack Vector & Proof of Concept
An attacker authenticates legitimately as User A (ID: 1042) and notices an endpoint returning user billing profiles: `GET /api/v1/accounts/1042/statement`. By mutating the path parameter or JSON body to `GET /api/v1/accounts/1043/statement`, the API responds with User B's full financial statement and PII.
The root cause typically lies in the database querying layer. Developers frequently query the object solely by its primary key without scoping the query to the authenticated user context or tenant identifier.
GET /api/v2/workspaces/8819/audit-logs HTTP/1.1
Host: api.target-platform.internal
Authorization: Bearer eyJhbGciOiJIUzI1Ni...
User-Agent: SecurityResearch/1.0
Accept: application/jsonHTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 1420
{
"workspace_id": "8819",
"owner_email": "target-admin@corp.domain",
"admin_actions": [
{ "timestamp": "2026-03-14T19:22:10Z", "action": "revoke_api_key", "actor": "ops-lead" }
]
}03.Source Code Analysis
// ❌ VULNERABLE: Only checking object ID from client input
async function getInvoice(req: Request, res: Response) {
const { invoiceId } = req.params;
const invoice = await db.invoices.findOne({ id: invoiceId });
if (!invoice) return res.status(404).json({ error: 'Not found' });
return res.json(invoice); // Leaks invoice across tenants!
}
// ✅ SECURE: Enforcing user and tenant ownership in the query
async function getInvoice(req: AuthenticatedRequest, res: Response) {
const { invoiceId } = req.params;
const currentTenantId = req.user.tenantId;
const invoice = await db.invoices.findOne({
id: invoiceId,
tenantId: currentTenantId, // Strict tenancy isolation
});
if (!invoice) {
return res.status(404).json({ error: 'Invoice not found' });
}
return res.json(invoice);
}04.Impact & Risk Assessment
Complete compromise of confidentiality across all customer tenants, potential data modification or deletion, and regulatory non-compliance (GDPR, HIPAA).
05.Remediation & Engineering Mitigations
- Enforce authorization checks at the database query level by always scoping queries to the active session tenant ID.
- Adopt unpredictable randomized identifiers (UUIDv4) to prevent sequential enumeration, although UUIDs alone do not substitute for authorization.
- Implement automated integration tests asserting cross-tenant access denial (HTTP 403/404) for every endpoint receiving resource IDs.
- Use policy-as-code engines (e.g., Open Policy Agent / Cedar) to decouple authorization policies from business logic.