The admin panel is beautiful. It only shows up for users with role: 'admin', and the button is tucked behind a clean little feature flag. But the API routes behind those buttons don't know they're admin routes. Any logged-in user can call them directly. The guard lived in the frontend the whole time.
Function-level authorization is the question 'is this caller allowed to use this feature at all?' — as distinct from BOLA's 'is this caller allowed to touch this object?' When the answer lives in the UI instead of the route handler, any logged-in user becomes effectively an admin the moment they know the URL.
What your AI actually built
You asked for an admin dashboard with user management, org settings, and a feature toggle panel. The model built all three. It added a nice sidebar that only renders for admins, and it wrote the corresponding API routes: DELETE /api/users/:id, POST /api/orgs/:id/plan, PATCH /api/flags/:name.
What it did not do is add a role check on any of those routes. Each handler only verifies the caller is logged in. 'The UI hides the button from non-admins' was the implicit assumption. The UI is a suggestion; the API is the boundary.
On top of that, there are usually hidden endpoints the UI never even calls — /api/internal/debug/users, /api/admin/impersonate — that the model added 'just in case' and nobody added auth to because nobody was testing them.
How it gets exploited
A regular user with a paid account opens the browser dev tools and views the JavaScript bundle.
- 1Read the routes off the clientThe bundled React code contains references to every admin endpoint, including the ones hidden behind conditionals. They copy-paste a list: /api/admin/users, /api/admin/impersonate, /api/admin/flags.
- 2Call them directlyThey send a POST to /api/admin/impersonate with { userId: 1 }. The server issues them a token as user 1 — the root admin — because nothing in that route checked whether the caller was allowed to use it.
- 3Toggle the flagsAs the root admin, they flip the 'require_payment' flag to false, grant themselves unlimited credits, and disable audit logging on their own account. Every request is authenticated, every request is logged (until the last step). Nothing looks wrong.
One regular account turned into full ownership of the platform. The vulnerability is not a bypass — the attacker used the admin feature exactly as built. The only thing missing was the check that said 'you are not an admin.'
Vulnerable vs Fixed
// app/api/admin/users/[id]/route.ts
export async function DELETE(req, { params }) {
const session = await getSession(req);
if (!session) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
// Any logged in user can call this. The UI hides the button, so
// "obviously" only admins will reach it. The API does not agree.
await db.user.delete({ where: { id: params.id } });
return Response.json({ ok: true });
}// app/api/admin/users/[id]/route.ts
import { requireAdmin } from '~/lib/auth';
export async function DELETE(req, { params }) {
const admin = await requireAdmin(req); // throws 403 if not admin
if (admin instanceof Response) return admin;
await db.audit.create({
data: { actorId: admin.id, action: 'user.delete', targetId: params.id },
});
await db.user.delete({ where: { id: params.id } });
return Response.json({ ok: true });
}Function-level authorization goes at the function — the route handler — not at the button that calls it. The `requireAdmin` helper forces every admin route to make the check explicitly, so you can't forget. The audit log is the other half: if an admin function runs, record who ran it.
A real case
A Venmo bug let any user list every transaction between every other user
For years, Venmo's API exposed a public transactions endpoint that required no admin role — researchers scraped 200 million transactions before the endpoint was locked down. Same pattern: a function that was never meant for anyone to call directly was left wide open.
Related reading
References
Find the admin routes that never checked for admins.
Flowpatrol discovers every endpoint in your app and tests each one against a low-privilege session. The bugs your UI hides, the scanner finds.
Try it free