Introduction & Context
In the OWASP API Security Top 10, API1:2023 - Broken Object Level Authorization (BOLA) — historically known as IDOR (Insecure Direct Object Reference) — holds the #1 spot.
It is the single most devastating security vulnerability in modern web applications, SaaS platforms, and REST/GraphQL APIs. BOLA occurs when an API endpoint accepts an object identifier (e.g., /api/documents/8492 or a GraphQL mutation updateUser(id: "8492")) without verifying whether the authenticated user has legitimate ownership or access permissions over that specific object.
1. Why Authentication (JWT/OAuth) Does NOT Protect Against BOLA
A common misconception among developers is assuming that a valid authentication middleware (e.g., a valid JWT returning HTTP 200) automatically secures an API endpoint.
┌─────────────────────────┐
│ HTTP Request (Bearer JWT)│ ──> [ Auth OK ] (User is 'Bob')
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ Request: /api/orders/99 │ ──> [ No BOLA check! ] ──> Returns Alice's order!
└─────────────────────────┘
- Authentication: Answers "Who is this user?" (e.g., Bob).
- Object-Level Authorization (BOLA): Answers "Does Bob have the right to read/modify object #99 which belongs to Alice?".
2. Concrete Examples & Vulnerable Code
❌ Example 1: Next.js / Node.js REST Route Handler with Classic BOLA Vulnerability
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { verifyJwt } from "@/lib/auth";
export async function GET(request: Request, { params }: { params: { invoiceId: string } }) {
const authHeader = request.headers.get("authorization");
const user = verifyJwt(authHeader);
// ❌ User is authenticated...
if (!user) {
return NextResponse.json({ error: "Unauthenticated" }, { status: 401 });
}
// ❌ CRITICAL BOLA VULNERABILITY: Fetching invoice ONLY by its ID!
// If Bob enters Alice's invoice ID (invoiceId = "inv_9999"), the server returns it!
const invoice = await db.invoice.findUnique({
where: { id: params.invoiceId }
});
if (!invoice) {
return NextResponse.json({ error: "Invoice not found" }, { status: 404 });
}
return NextResponse.json(invoice);
}
- Attack Scenario: An attacker signs up, gets their own invoice
inv_0001, and runs a script iterating overinv_0002,inv_0003... stealing financial data across all accounts in seconds.
❌ Example 2: GraphQL Mutation Vulnerable to BOLA
# Exposed GraphQL Mutation
mutation DeleteUserDocument($documentId: ID!) {
deleteDocument(id: $documentId) {
success
}
}
// Vulnerable GraphQL Resolver
const resolvers = {
Mutation: {
deleteDocument: async (_, { id }, context) => {
if (!context.user) throw new Error("Unauthenticated");
// ❌ BOLA VULNERABILITY: Deleting directly by ID without verifying context.user.id
await db.document.delete({ where: { id } });
return { success: true };
}
}
};
3. Complete Remediation & Secure Code
To permanently eliminate BOLA vulnerabilities, enforce this golden rule: Every database read, update, or delete operation MUST include the active session's userId or organizationId in the query condition.
✅ Corrected REST Route (Strict Object-Level Authorization)
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { verifyJwt } from "@/lib/auth";
export async function GET(request: Request, { params }: { params: { invoiceId: string } }) {
const authHeader = request.headers.get("authorization");
const user = verifyJwt(authHeader);
if (!user) {
return NextResponse.json({ error: "Unauthenticated" }, { status: 401 });
}
// ✅ BOLA FIX: Query condition combines resource ID AND owner ID
const invoice = await db.invoice.findFirst({
where: {
id: params.invoiceId,
userId: user.id, // 🔒 Only invoices owned by the authenticated user will be returned
}
});
if (!invoice) {
// Returning 404 instead of 403 prevents revealing the existence of other users' resources
return NextResponse.json({ error: "Invoice not found" }, { status: 404 });
}
return NextResponse.json(invoice);
}
✅ Role-Based Access Control (RBAC / ABAC Integration)
If an administrator needs access to any invoice:
export async function getInvoiceSecure(invoiceId: string, currentUser: { id: string; role: string }) {
// 1. If user is ADMIN, allow fetching without owner constraint
if (currentUser.role === "ADMIN") {
return await db.invoice.findUnique({ where: { id: invoiceId } });
}
// 2. Otherwise (REGULAR USER), enforce owner constraint
return await db.invoice.findFirst({
where: {
id: invoiceId,
userId: currentUser.id
}
});
}
4. Migration to Non-Predictable Cryptographic IDs (CUID2 / UUID v4)
❌ Vulnerable Prisma Schema (Auto-incrementing IDs easy to enumerate)
model Invoice {
id Int @id @default(autoincrement()) // ❌ Ex: 1, 2, 3, 4... Trivial enumeration
}
✅ Secure Prisma Schema (Random CUID2 identifiers)
model Invoice {
id String @id @default(cuid()) // ✅ Ex: clh1g98x200003567y8z9a0bc
userId String
user User @relation(fields: [userId], references: [id])
}
BOLA Audit Checklist for Dev & QA Teams
- [ ] Audit all endpoints with URL parameters: Ensure
GET /api/resource/:id,PUT,DELETEfilter by authenticateduserId. - [ ] Automated Authorization Testing: Write integration tests (Jest / Playwright) where User A attempts to access User B's resource and expects
404 / 403. - [ ] Disable auto-incrementing IDs: Use non-predictable UUID v4 or CUID2 identifiers.