# Protected route groups actually call a session getter

- **Pattern:** `ab-002611` (`project-snapshot.security.protected-routes-call-session-getter`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-23
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002611
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002611)

## 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:
  ```tsx
  // 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 each `page.tsx` / `layout.tsx` / `route.ts`. For each, check whether the file itself OR its closest parent `layout.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 using `useSession()` / `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:**
  ```tsx
  // 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}</>;
  }
  ```
  A single layout gate covers every descendant page.

## External references

- cwe CWE-862
- cwe CWE-306
- owasp A01
- owasp A07

Taxons: project-snapshot, access-control

HTML version: https://auditbuffet.com/patterns/ab-002611
