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), notuser_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
- Supabase: Managing User Data (app_metadata)
- OWASP: Broken Access Control (A01:2021)
- CWE-285: Improper Authorization
Related Checks
- Server-Side Auth for Protected Routes — AUTH-04, ADM-01
- RBAC & Role Claims Design — AUTH-18, ADM-05
- MFA for Admin Roles — ADM-13
Is your app safe? Run Free Scan →