Protected route groups actually call a session getter
Why it matters
AI coding tools love the Next.js parens-prefix convention — they will happily scaffold app/(authenticated)/dashboard/page.tsx or app/(app)/settings/page.tsx and then forget that the parenthesized segment is purely a routing grouping, not a security boundary. The directory name carries no runtime effect; if the page file itself never calls auth(), getServerSession(), or supabase.auth.getUser(), anyone who guesses the URL gets the page rendered, HTML streamed, and server-component data leaked. Facebook's 2018 token-flaw breach exposed 50 million accounts through a similar gap between "this route is supposed to be protected" and "this route actually checks the session server-side." OWASP ranks Broken Access Control as the #1 web risk in its 2021 Top 10 precisely because this class of failure is this easy to ship. The failure is especially cruel because the app feels secure during development — the team tests logged-in, sees the dashboard, ships. The anonymous-traffic path is never exercised until an attacker finds it.
Severity rationale
Critical because an unguarded protected-group route streams private user data and admin surfaces to anonymous requests — a complete access-control failure with zero mitigation until the session getter is wired in.
Remediation
Add a server-side session check at the top of every protected route group's layout file, and have the layout redirect unauthenticated requests before any child route renders:
// app/(authenticated)/layout.tsx
import { redirect } from 'next/navigation';
import { auth } from '@/lib/auth';
export default async function AuthenticatedLayout({ children }: { children: React.ReactNode }) {
const session = await auth();
if (!session?.user) redirect('/login');
return <>{children}</>;
}
A single layout gate covers every descendant page.tsx and nested layout.tsx — you do not need a check in each file if the parent layout redirects. Client-only guards ('use client' + useSession()) are not sufficient because the server still ships the protected HTML before the client-side check runs. For the full authorization pass including role checks, route-level RBAC, and admin-action audit logging, run the saas-authentication and security-hardening Pro audits.
Detection
- ID:
protected-routes-call-session-getter - Severity:
critical - What to look for: Enumerate files under parens-prefixed auth-named route groups:
app/(authenticated)/,app/(app)/,app/(dashboard)/,app/(protected)/,app/(private)/,app/(user)/. Walk eachpage.tsx/layout.tsx/route.ts. For each, check whether the file itself OR its closest parentlayout.tsx(walking upward) calls a session getter:auth(),getServerSession(),getSession(),supabase.auth.getUser(),requireUser(),requireAuth(),currentUser(),clerkAuth(),getKindeServerSession(). - Pass criteria: Every file in every auth-named group is covered — directly or by a parent layout.
- Fail criteria: Any file has no session getter in itself or any ancestor layout. A
'use client'component usinguseSession()/useUser()does NOT count — the protected HTML has already streamed before the client check runs and is bypassable. - Skip (N/A) when: No parens-prefixed auth-named route groups exist. Quote the directory list.
- Before evaluating, quote: The route-group directory list + first 20 lines of each flagged file and every ancestor layout inspected.
- Report even on pass: Route-group count + layout gate per group:
"(authenticated) covered by app/(authenticated)/layout.tsx: auth() + redirect('/login')". - Detail on fail:
"app/(dashboard)/settings/page.tsx renders with no server session check; app/(dashboard)/layout.tsx missing entirely; parens grouping is routing-only". - Remediation:
A single layout gate covers every descendant page.// app/(authenticated)/layout.tsx import { redirect } from 'next/navigation'; import { auth } from '@/lib/auth'; export default async function Layout({ children }: { children: React.ReactNode }) { const session = await auth(); if (!session?.user) redirect('/login'); return <>{children}</>; }
External references
- cwe · CWE-862 — Missing Authorization
- cwe · CWE-306 — Missing Authentication for Critical Function
- owasp:2021 · A01 — Broken Access Control
- owasp:2021 · A07 — Identification and Authentication Failures
Taxons
History
- 2026-04-23·v1.0.0·Initial Phase 9.1 v3.1 Stack Scan promotion — AI-generated apps route-group files often lack server-side session checks, rendering private pages to anonymous traffic.·by phase-9-1-stack-scan-v3-1