ASAASA Standard
Not in Phase 1Production Foundation

No Client-Side-Only Role Checks

Admin Safety · ADM-03 · Priority: P0

Why It Matters

When role checks happen only in the browser — if (user.role === 'admin') in a React component — any user can bypass them. Browser DevTools, localStorage modification, or a simple API call with a forged header grants full admin access.

In a security audit by Infinum, researchers found that changing a single "role" field from "user" to "admin" granted full administrative privileges in an AI-built application. No server-side validation existed. NetSPI's penetration test confirmed the same pattern: AI tools implement client-side-only role checks using manipulatable HTTP headers.

Priority: P0 — Privilege escalation to admin. Full application compromise.

Affected Stack: Next.js, Supabase, any framework with role-based access


The Problem

AI tools generate role-based UI that hides admin features from non-admin users — but the API endpoints behind those features have no role verification.

// ❌ Client-side only — role check in React component
function AdminPanel() {
  const { user } = useAuth();
  if (user.role !== 'admin') return <p>Access denied</p>;

  return (
    <div>
      <button onClick={() => fetch('/api/admin/delete-user', { method: 'POST' })}>
        Delete User
      </button>
    </div>
  );
}

// ❌ API route has NO role check
export async function POST(req: Request) {
  const { userId } = await req.json();
  await db.users.delete({ where: { id: userId } });
  return new Response('Deleted');
}

The admin panel is hidden in the UI, but /api/admin/delete-user is accessible to anyone.


The Fix

Every admin API route must independently verify the user's role on the server, using data from Supabase app_metadata (which users cannot modify themselves).

// ✅ Server-side role verification
export async function POST(req: Request) {
  const supabase = createServerClient(/* ... */);
  const { data: { user } } = await supabase.auth.getUser();

  if (!user) return new Response('Unauthorized', { status: 401 });

  // Role from app_metadata — set by server only, not user-editable
  const role = user.app_metadata?.role;
  if (role !== 'admin') {
    return new Response('Forbidden', { status: 403 });
  }

  const { userId } = await req.json();
  await supabase.from('users').delete().eq('id', userId);
  return new Response('Deleted');
}

Key rules:

  • Store roles in app_metadata (server-only), not user_metadata (user-editable)
  • Never trust role information from the client request body or headers
  • The client-side check is a UX convenience — the server-side check is the security boundary
  • Log all role check failures for security monitoring

References


Related Checks


Is your app safe? Run Free Scan →