Stop 19 of 42 · Account Access · Privilege escalation

Walking into the manager’s office

Mallory is an ordinary customer. The manager’s office has a rule: managers only. But the teller asks Mallory, “what’s your role?” and takes her word for it. She writes “admin” on a slip and walks right in.

Severity Medium to CriticalCategory Privilege EscalationAccount AccessSources checked 25 Oct 2026

Watch it happen

Play it as

Mallory is a regular customer. She writes “role: admin” on a slip, the teller believes it, and the admin office opens.

Arrival

Mallory is a regular customer with a regular badge. She walks in and can do customer things, and that is fine.

Mallory says: Hi, I’m Mallory.
1 / 5

In bank terms and app terms

In the bankIn your app
Role on the slipA role, isAdmin flag or plan sent in the request, cookie or token you do not verify
Staff recordsThe role stored on the server (your profiles table), read fresh for each request
Manager’s officeAdmin routes, admin tables and privileged database functions (RPC)
Teller checking the recordsA role check on every admin route, backed by database rules

Are you exposed?

You might be exposed if…
  • Your app decides who is an admin by a value the browser can send (a flag, a cookie, a header).
  • Admin pages are hidden in the menu, but the /admin API routes behind them do not check the role.
  • A user can update their own row in a profiles or roles table, including the role column.
What to do about it
  1. Read the user’s role on the server for each request and require "admin" on every admin route.
  2. Block users from editing role-like columns: keep roles in a table users cannot write, or use RLS plus column privileges or a trigger so only admins or the server can change them.
  3. Keep admin-only tables and functions behind policies, not behind a hidden URL.

Track these in your to-do list →

What it looks like in code

// GOOD: Check role on every admin endpoint
export async function GET(request: Request) {
  const user = await getAuthUser(request)
  if (!user || user.role !== "admin") {
    return NextResponse.json({ error: "Forbidden" }, { status: 403 })
  }
  // ... admin logic
}

// GOOD: Use Supabase RLS for role-based access
CREATE POLICY "Admins only" ON admin_settings
  FOR ALL USING (
    auth.uid() IN (SELECT id FROM profiles WHERE role = 'admin')
  );

Ask your AI tool to fix it

Paste this into Cursor, Claude, Lovable, Bolt or whatever you build with.

Prompt
Audit how this app decides who is an admin. Find every place a role, isAdmin or plan value comes from the request, a cookie or a client-writable column. Replace those with a server-side lookup of the verified user’s role, add a role check to every admin route, and make sure users cannot update their own role (for example with Supabase RLS policies, column privileges or a server-only roles table).

Where this comes from

The facts, figures and fixes on this page were checked against these sources on 25 Oct 2026. They are linked so you can check for yourself.

Spot a mistake or something out of date? Report a correction · How these guides are made and checked