Auth Check Only in Client Code
The auth gate runs only in a client component (useEffect redirect or conditional render), which an attacker bypasses by disabling JavaScript or hitting the API route directly.
Typical error
Admin route protected only by client-side redirect
What this is
A common AI-generated pattern:
'use client'
import { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { useAuth } from '@/hooks/useAuth'
export default function AdminPage() {
const { user } = useAuth()
const router = useRouter()
useEffect(() => {
if (!user || !user.isAdmin) router.push('/')
}, [user, router])
if (!user?.isAdmin) return null
return <AdminDashboard />
}The render happens on the client, which means:
- The page HTML ships to the visitor before auth runs
- Disabling JavaScript shows the content
- Any API route the admin page calls is not protected by this check
Why AI tools ship this
Client-side redirects are easier to write and debug. The generated code works visually (non-admins get bounced to /), so the tool declares victory.
How to detect
Look for auth-gated pages that are 'use client' components and rely on useEffect redirects. Also check that every server route the page calls enforces the same check.
How to fix
Enforce auth at the server boundary.
-
Server component page: check the session in the page itself.
import { redirect } from 'next/navigation' import { getServerSession } from '@/lib/auth' export default async function AdminPage() { const session = await getServerSession() if (!session?.user?.isAdmin) redirect('/') return <AdminDashboard /> } -
API routes: every admin route must call the same check, not trust a header from the client.
-
Middleware: for uniform enforcement across
/admin/*, usemiddleware.tsto gate the whole subtree.
Return 404 instead of 403 for admin routes so the route's existence is not leaked to non-admins.
Related
- Glossary: auth bypass