When I audit Next.js apps built quickly (often with AI tools), one pattern shows up more than any other: routes are protected in middleware.ts, and the API route handlers trust that.
// app/api/admin/users/route.ts
export async function GET(request: NextRequest) {
// "middleware already checked"
const users = await db.user.findMany()
return NextResponse.json(users)
}
Middleware can be skipped by a matcher that doesn't cover a new route, misconfigured during a refactor, or bypassed by calling the API directly. If the handler doesn't check, the data is open.
The fix is boring: every handler that touches user data verifies the session and role itself.
export async function GET(request: NextRequest) {
const session = await getServerSession(authOptions)
if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
if (session.user.role !== 'admin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
const users = await db.user.findMany()
return NextResponse.json(users)
}
Two related checks worth doing while you're there:
- Hiding the admin dashboard in React when
role !== 'admin'is UI gating, not access control. The API behind it still needs the server-side check. - Delete a user's session row and make a request with their old cookie. If it still works, revocation isn't wired up.
Rule of thumb: middleware is a convenience layer for redirects and early exits. The security boundary is the handler.
How do you handle this in your apps: a shared requireUser() helper, or checks inline in each route?
Top comments (0)