← OWASP Top 10:2025

A01:2025

Broken Access Control

Users can act outside their intended permissions: read other people's data, reach admin pages, call endpoints they should not.

What it is

Access control is the check that runs after login: is this user allowed to do this thing to this object? Broken access control is any way that check can be skipped. A route that renders the dashboard without confirming there is a session. An API that returns an order when you change the ID in the URL. An admin action that any authenticated user can trigger. A file endpoint that serves whatever path you ask for.

OWASP has ranked it first since 2021 and it stays first in the 2025 list, which also folds server-side request forgery into it. It ranks first because it is common, easy to exploit with nothing but a browser, and the impact is direct: someone else's data, on screen, with no exploit involved.

How it shows up in AI-generated code

This is the single most common serious flaw in AI-built apps, and it comes from a specific habit. Ask a coding tool to add login and it builds the login page, the session, the redirect. That is authentication. It routinely skips authorization, the check on every route and endpoint that decides what the logged-in user may touch, because nothing in the prompt asked for it and the app works without it.

We measured it. We requested the private routes of 4,740 deployed AI-built apps with no session: 159 rendered a logged-in page to a logged-out visitor, 72 returned real records from an API with no auth header, and 30 exposed a database readable with the key already in the browser. That is 261 apps, about 1 in 18, with no exploit and no password guessing. The full anonymised dataset is public on the research page.

The same pattern shows up in source. In 2,148 scanned repos, missing access control on a route with a sensitive operation was the most frequent medium finding, and it clustered in exactly the files AI tools scaffold: CRUD handlers, serverless functions, and API routes generated from a one-line description.

Example: An API route that trusts the ID in the URL

The pattern
// app/api/orders/[id]/route.ts
export async function GET(req: Request, { params }) {
  const order = await db.order.findUnique({ where: { id: params.id } });
  return Response.json(order);   // any user, any order
}
The fix
export async function GET(req: Request, { params }) {
  const session = await getSession(req);
  if (!session) return new Response("Unauthorized", { status: 401 });
  const order = await db.order.findUnique({
    where: { id: params.id, userId: session.user.id },  // scoped to the caller
  });
  if (!order) return new Response("Not found", { status: 404 });
  return Response.json(order);
}

The fix is two checks, not one: is there a session, and does the object belong to this user. AI tools often add the first and skip the second, which is the insecure direct object reference (IDOR) variant.

How to find it

  • Open a private page in a logged-out tab. If it renders instead of redirecting, that is the finding.
  • Change an ID in a URL or API call to someone else's. If it returns their data, that is IDOR.
  • Call your own /api routes with curl and no cookie or token. Anything that returns records is exposed.
  • For Supabase and Firebase, check whether row level security or rules are enforced on every table, not just the ones you remembered.
  • Prbl does all four on a live URL: the open-route probe on every scan, the API and database reads for verified owners, and rule PRBL-A001 on the repo.

How to fix it

  • Enforce authorization in one place that every route passes through: middleware, a route wrapper, or a policy layer. Per-route checks get forgotten; a wrapper does not.
  • Deny by default. A new route should be private until someone marks it public, not the reverse.
  • Scope every database query by the caller: where userId = session.user.id, or row level security that does the same thing server-side.
  • Never trust IDs, roles or flags that come from the client. Read them from the session.
  • Turn on row level security for every Supabase table and write a policy for each; the anon key is public by design and RLS is the only thing between it and your data.

What Prbl checks for this category

PRBL-A001 Missing access controlPRBL-T001 Path traversalURL-UNAUTH-ROUTE live probeURL-UNAUTH-DATA owner probeURL-SUPABASE-RLS-OFF owner probe

Run a free scan on a public repo or a live URL. Findings link to the fix guides below.

Fix guides for this category

Go deeper

Common questions

Is having a login page enough?

No. A login page proves the app can identify you. Access control is the separate check, on every route and query, that decides what you may see once identified. Most AI-built apps have the first and are missing the second on at least one surface.

What is the difference between broken access control and broken authentication?

Authentication failures are about proving who you are: weak passwords, no rate limiting, sessions that never expire. Access control failures happen after that: you are correctly logged in as you, and the app still lets you touch things that are not yours. OWASP lists them separately as A01 and A07.

Does Supabase row level security count as access control?

Yes, and for apps that talk to Supabase from the browser it is the access control. The anon key is public by design; RLS policies are what stop it from reading every row. With RLS off on a table, anyone with the key from your page can read that table directly, bypassing your app entirely.

Is this category already in something you shipped? Scan a live URL or a public repo free, no account.

Scan my app →
A01:2025 Broken Access Control Explained, With AI-Generated Code Examples | Prbl