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
apps/web/app/api/orders/[id]/route.ts:21Broken Object-Level Authorization (BOLA) in Next.js apps
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.
// 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);
}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.
Related guides
IDOR is the classic name for an authorization bug where a user can change an ID in a URL and access data they should not see. It is BOLA's older cousin and still ships in half of all new apps.
Every week founders tweet about their OpenAI bill going from $10 to $10,000 overnight. Usually the cause is an API key committed to a public repo. Here is why it happens in Next.js specifically and how to stop it in five minutes.
Unlimited API endpoints are how $150K OpenAI bills happen. Here is how to add proper rate limiting to a Next.js app using Vercel Edge Middleware, Upstash, or your existing Redis.
CORS is one of the most misunderstood security headers. Here is exactly when `*` is safe, when it is catastrophic, and how to configure CORS correctly for a Next.js + Supabase stack.