Broken Object Level Authorization (BOLA)
Prevent users from accessing objects that do not belong to them by manipulating IDs. Rules for Cursor, Claude Code, and Codex.
URL: trusteed.io/academy/api-security/broken-object-level-authorization
Broken Object Level Authorization (BOLA) — sometimes called IDOR (Insecure Direct Object Reference) — occurs when an API endpoint retrieves or modifies an object using an ID supplied by the client, without verifying that the requesting user actually has permission to access that specific object. It is the single most common and most exploited API vulnerability class, and it's also the pattern AI coding assistants reproduce most reliably, because "fetch the record matching this ID" is the simplest, most natural way to implement almost any object-lookup endpoint.
What Is BOLA?
Every object-level endpoint — GET /api/orders/{id}, PUT /api/documents/{id}, DELETE /api/invoices/{id} — needs to answer two separate questions: does this object exist, and does the currently authenticated user have the right to access this specific object. BOLA happens when an implementation only answers the first question.
The exploit is trivial: an authenticated attacker simply increments or guesses an ID — /api/orders/1042 becomes /api/orders/1043 — and if the backend doesn't check ownership, they retrieve, modify, or delete someone else's data using their own valid credentials. No privilege escalation, no stolen token — just a missing ownership check on an otherwise correctly authenticated request.
Why AI Coding Assistants Get This Wrong
Ask an AI assistant to "add an endpoint to fetch an order by ID," and the fastest, most natural completion is a direct database lookup: find the record where the ID matches, return it. That's functionally correct and it's what nearly every tutorial, Stack Overflow answer, and boilerplate generator does — because demonstrating a database query is the point of the example, not demonstrating an authorization model the example doesn't have.
The assistant has no way to know your authorization model unless you tell it: whether orders belong to users, whether admins can see all orders, whether there's a tenant boundary. Without that context stated explicitly, the default generated code will look up by ID alone.
Vulnerable Pattern
// Express.js — looks correct, has no ownership check
app.get('/api/orders/:id', authenticate, async (req, res) => {
const order = await Order.findById(req.params.id);
if (!order) return res.status(404).json({ error: 'Not found' });
res.json(order); // any authenticated user can read ANY order
});
Secure Pattern
// Express.js — scoped to the authenticated user's ownership
app.get('/api/orders/:id', authenticate, async (req, res) => {
const order = await Order.findOne({
_id: req.params.id,
userId: req.user.id, // ownership enforced in the query itself
});
if (!order) return res.status(404).json({ error: 'Not found' });
res.json(order);
});
Scoping ownership inside the query (rather than fetching first and checking after) is deliberate: it prevents timing-based enumeration and ensures a missing check can never accidentally leak existence of records the user shouldn't even know about. For role-based access (e.g., admins can see all orders, support agents can see orders in their assigned region), extend the query filter conditionally based on req.user.role and req.user.scope — never based on a client-supplied field.
Copy-Paste Rules for Cursor, Claude Code, and Codex
Paste the block below into .cursor/rules/api-authorization.mdc (Cursor), CLAUDE.md (Claude Code), or AGENTS.md (Codex and other agentic tools). Adjust the ownership field names to match your schema.
---
description: BOLA prevention — object-level authorization on every ID-based endpoint
alwaysApply: true
---
# Object Level Authorization Rules
- Every endpoint that accepts an object ID (`:id`, `orderId`, `documentId`, etc.)
MUST verify the authenticated user is authorized to access that specific object
— never rely on ID lookup alone.
- Enforce ownership/scope INSIDE the database query (e.g.,
`findOne({ _id, userId: req.user.id })`), not as a separate check after fetching.
- Never trust a role, tenant ID, or ownership field if it is supplied by the
client (request body, query string, headers). Always derive it from the
authenticated session/token.
- For role-based access, extend the query filter based on `req.user.role`
server-side — do not add a bypass branch based on a client-supplied flag.
- Return 404 (not 403) for objects the user doesn't own, to avoid confirming
the object's existence to unauthorized users, unless your threat model
specifically requires 403.
- When generating a new object-level endpoint, always ask or state explicitly
what the ownership/tenancy model is before writing the query.
- Flag any generated code that fetches an object by ID without a visible
authorization filter as incomplete, not done.
Verification Checklist
- Every
GET,PUT,PATCH,DELETEendpoint accepting an object ID enforces ownership in the query, not just in application logic after the fetch - Ownership/tenant fields are never read from client-supplied input
- Automated tests exist for "user A cannot access user B's object" for every object-level endpoint
- IDs are non-sequential (UUIDs) where feasible, as defense in depth — not a substitute for authorization checks
- Continuous API testing validates this at runtime — see Trusteed's WAAP platform, which combines automated and manual testing designed to catch BOLA and other OWASP API Top 10 categories that unit tests typically miss
Frequently asked questions
Does using UUIDs instead of sequential IDs fix BOLA?
No. Non-sequential IDs make guessing harder but don't add authorization. If an attacker obtains a valid UUID through any other channel (a leaked link, a referral, another vulnerability), the missing ownership check is still exploitable.
Can automated scanners reliably find BOLA?
Generic scanners struggle because BOLA requires understanding your specific ownership model — testing "does user A's token let them access user B's resource" needs authenticated, role-aware test cases. This is why manual and business-logic-aware testing, not just automated signature scanning, is necessary to catch BOLA reliably.
How is BOLA different from BFLA?
BOLA is about accessing the wrong *object* (someone else's specific record). BFLA is about accessing the wrong *function* (an admin-only endpoint) — see [API5:2023](https://trusteed.io/academy/api-security/broken-function-level-authorization).