Broken Object-Level Authorization (BOLA) in Next.js apps
BOLA is the top item on the OWASP API Security Top 10 for a reason — every AI coding assistant introduces it by default. Learn what it looks like in Next.js, how to exploit it, and how to fix it.
You asked your AI coding tool for a route that returns an order by its ID. It wrote something like `/api/orders/[id]` that looks up the order and returns it. If you shipped that, you probably have BOLA — Broken Object-Level Authorization — the single most common API vulnerability in production.
What it is
BOLA (sometimes called IDOR — Insecure Direct Object Reference) happens when an API exposes an object by its identifier and does not verify that the requesting user is authorized to see that specific object. Any authenticated user can usually change a number in the URL and read someone else's data.
Vulnerable example
// app/api/orders/[id]/route.ts — vulnerable: fetches without authz check
import { createClient } from "@/lib/supabase/server";
export async function GET(req: Request, { params }: { params: { id: string } }) {
const supabase = await createClient();
const { data, error } = await supabase
.from("orders")
.select("*")
.eq("id", params.id)
.single();
if (error) return new Response("not found", { status: 404 });
return Response.json(data);
}Fixed example
// Fixed: RLS on the table + explicit user scoping in the query
import { createClient } from "@/lib/supabase/server";
export async function GET(req: Request, { params }: { params: { id: string } }) {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return new Response("unauthorized", { status: 401 });
const { data, error } = await supabase
.from("orders")
.select("*")
.eq("id", params.id)
.eq("user_id", user.id)
.single();
if (error) return new Response("not found", { status: 404 });
return Response.json(data);
}How Securie catches it
The BOLA specialist agent inventories every route that accepts an opaque identifier and models the authorization boundary it should enforce. Then the verification sandbox issues requests to that route as user-A using user-B's identifiers. If a response is returned, it is a proven BOLA. The fix emits as a pull-request comment with both the route-level authz check and the corresponding Supabase RLS policy.
Checklist
- Every API route that accepts an ID verifies the requesting user owns that object
- Authorization checks happen in both the API layer and the database (defense in depth)
- Integration tests cover cross-user access attempts, not just happy-path reads
- Pagination endpoints do not leak IDs the user cannot access
- Object IDs are UUIDs (not incrementing integers) to reduce enumeration risk
- Failed authorization returns a 404 or 403 consistently — never 200 with an empty body
FAQ
Is using UUIDs instead of integer IDs enough?
No. UUIDs make enumeration harder but do nothing to stop an attacker who obtains a valid ID through a referrer leak, a shared link, or another API endpoint.
What about JWT-based auth? Doesn't that prevent BOLA?
JWT establishes the user's identity. It does not automatically authorize each object access. You still need an explicit ownership check on every route.