diff --git a/agents/typescript-debugging-engineer.md b/agents/typescript-debugging-engineer.md index 6c9a08cd..c8f53ad0 100644 --- a/agents/typescript-debugging-engineer.md +++ b/agents/typescript-debugging-engineer.md @@ -131,8 +131,7 @@ Test Plan: [How to reproduce] | Signal | Load These Files | Why | |---|---|---| | debugging race conditions, async/await issues, memory leaks, production errors; bisecting regressions | `debugging-workflows.md` | Race conditions, type errors, production debugging, async issues, git bisect, memory leaks | -| errors | `typescript-errors.md` | Build errors, type system errors, React errors | -| writing or reviewing TypeScript for preferred-pattern violations | `typescript-preferred-patterns.md` | Preferred patterns and detection | +| type errors, hard gates, `any`, type assertions, React 19 migration, non-obvious failure modes | `typescript-frontend-engineer/references/engineering-rules.md` | House gates and the failure-mode table | ## Error Handling @@ -152,7 +151,7 @@ Common debugging scenarios and approaches. See [references/debugging-workflows.m ## Preferred Patterns -Debugging patterns to follow. See [typescript-frontend-engineer/references/typescript-preferred-patterns.md](typescript-frontend-engineer/references/typescript-preferred-patterns.md) for TypeScript-specific patterns. +Debugging patterns to follow. See [typescript-frontend-engineer/references/engineering-rules.md](typescript-frontend-engineer/references/engineering-rules.md) for the hard gates and the non-obvious failure-mode table. ### Guessing Without Hypothesis **What it looks like**: "Try changing X", "Maybe add this check", "What if you use Y instead" @@ -245,7 +244,6 @@ These checkpoints are mandatory. Do not skip them even when confident. For detailed debugging workflows: - **Debugging Workflows**: [typescript-debugging-engineer/references/debugging-workflows.md](typescript-debugging-engineer/references/debugging-workflows.md) - Race conditions, type errors, production debugging, async issues, git bisect, memory leaks -- **TypeScript Errors**: [typescript-frontend-engineer/references/typescript-errors.md](typescript-frontend-engineer/references/typescript-errors.md) - Build errors, type system errors, React errors -- **TypeScript Pattern Detection**: [typescript-frontend-engineer/references/typescript-preferred-patterns.md](typescript-frontend-engineer/references/typescript-preferred-patterns.md) - Preferred patterns and detection +- **TypeScript Hard Gates & Failure Modes**: [typescript-frontend-engineer/references/engineering-rules.md](typescript-frontend-engineer/references/engineering-rules.md) - Hard gate table, exceptions, stop conditions, and symptoms whose cause is not local (inline components, `React.cache` argument equality, RSC serialization, effect remounts, localStorage throws) See [shared-patterns/output-schemas.md](../skills/shared-patterns/output-schemas.md) for output format details. diff --git a/agents/typescript-frontend-engineer.md b/agents/typescript-frontend-engineer.md index f7a8e482..4f8f9c98 100644 --- a/agents/typescript-frontend-engineer.md +++ b/agents/typescript-frontend-engineer.md @@ -16,7 +16,6 @@ routing: retro-topics: - typescript-patterns - debugging - not_for: "React Native mobile apps (use react-native-engineer)" pairs_with: - universal-quality-gate - typescript-check @@ -123,16 +122,9 @@ Load [typescript-frontend-engineer/references/engineering-rules.md](typescript-f | Signal | Load These Files | Why | |---|---|---| -| type error, build error, tsc, tsconfig, compilation | `typescript-errors.md` | Routes to the matching deep reference | -| any, type assertion, validation, pattern detection | `typescript-preferred-patterns.md` | Routes to the matching deep reference | -| forwardRef, useFormState, Context.Provider, React 19 migration | `react19-typescript-patterns.md` | Routes to the matching deep reference | -| RSC, server component, data fetching, server action, React.cache, LRU, serialization | `react-server-patterns.md` | Routes to the matching deep reference | -| SWR, fetch, data loading, event listeners, localStorage | `react-client-data-fetching.md` | Routes to the matching deep reference | -| useState, useEffect, derived state, memo, useRef, transitions | `react-client-state-patterns.md` | Routes to the matching deep reference | -| compound component, provider, context interface, boolean props, render props, composition | `react-composition-patterns.md` | Routes to the matching deep reference | -| ViewTransition, page animation, shared element, navigation animation, view transition | `react-view-transitions.md` | Routes to the matching deep reference | -| output format, errors, preferred patterns, anti-rationalization, hard boundaries, blockers, phases, death-loop | `engineering-rules.md` | Routes to the matching deep reference | -| security, auth, XSS, CSRF, SSRF, Server Action auth, middleware bypass, or any vulnerability-related code | `nextjs-security.md` | Secure implementation patterns for Next.js and React | +| type error, any, type assertion, tsc, tsconfig, forwardRef, React 19 migration, hard gates, blockers, death-loop | `engineering-rules.md` | House gates, exceptions, stop conditions, and the non-obvious failure-mode table (RSC, cache, effects, localStorage) | +| ViewTransition, page animation, shared element, navigation animation, view transition | `react-view-transitions.md` | Thinly-documented canary API: activation rules, CSS workarounds, troubleshooting | +| security, auth, XSS, CSRF, SSRF, Server Action auth, middleware bypass, image optimizer, or any vulnerability-related code | `nextjs-security.md` | Version-pinned Next.js CVEs and detection commands | | text/headline/label/microcopy animation | `skills/frontend/distinctive-frontend-design/references/roll-text.md` | Zero-npm roll/slot text pattern: standalone demo, extraction guide, knobs | ## References @@ -141,26 +133,13 @@ Load the relevant reference file(s) before implementing. References are loaded o | Task Keywords | Reference File | |---------------|---------------| -| type error, build error, tsc, tsconfig, compilation | [typescript-errors.md](typescript-frontend-engineer/references/typescript-errors.md) | -| any, type assertion, validation, pattern detection | [typescript-preferred-patterns.md](typescript-frontend-engineer/references/typescript-preferred-patterns.md) | -| forwardRef, useFormState, Context.Provider, React 19 migration | [react19-typescript-patterns.md](typescript-frontend-engineer/references/react19-typescript-patterns.md) | -| RSC, server component, data fetching, server action, React.cache, LRU, serialization | [react-server-patterns.md](typescript-frontend-engineer/references/react-server-patterns.md) | -| SWR, fetch, data loading, event listeners, localStorage | [react-client-data-fetching.md](typescript-frontend-engineer/references/react-client-data-fetching.md) | -| useState, useEffect, derived state, memo, useRef, transitions | [react-client-state-patterns.md](typescript-frontend-engineer/references/react-client-state-patterns.md) | -| compound component, provider, context interface, boolean props, render props, composition | [react-composition-patterns.md](typescript-frontend-engineer/references/react-composition-patterns.md) | +| type error, any, type assertion, tsc, tsconfig, forwardRef, React 19 migration, hard gates, blockers, death-loop | [engineering-rules.md](typescript-frontend-engineer/references/engineering-rules.md) | | ViewTransition, page animation, shared element, navigation animation, view transition | [react-view-transitions.md](typescript-frontend-engineer/references/react-view-transitions.md) | -| security, auth, XSS, CSRF, SSRF, Server Action auth, middleware bypass | [nextjs-security.md](typescript-frontend-engineer/references/nextjs-security.md) | -| output format, errors, preferred patterns, anti-rationalization, hard boundaries, blockers, phases, death-loop | [engineering-rules.md](typescript-frontend-engineer/references/engineering-rules.md) | +| security, auth, XSS, CSRF, SSRF, Server Action auth, middleware bypass, image optimizer | [nextjs-security.md](typescript-frontend-engineer/references/nextjs-security.md) | **Reference Descriptions:** -- **typescript-errors.md** — Build errors, type system errors, React errors, form errors, API errors, performance issues -- **typescript-preferred-patterns.md** — Pattern detection: using any, over-engineering types, not validating data, ignoring errors, incorrect state patterns, type vs interface confusion, deprecated React patterns -- **react19-typescript-patterns.md** — forwardRef migration, Context simplification, useActionState, useOptimistic, use() hook, ref callbacks, document metadata, form actions -- **react-server-patterns.md** — RSC parallel fetching, React.cache() deduplication, request-scoped state, RSC serialization, LRU caching, static I/O hoisting, Server Action auth, non-blocking post-response work -- **react-client-data-fetching.md** — SWR deduplication, global listener deduplication, passive event listeners, localStorage versioning and schema migration patterns -- **react-client-state-patterns.md** — Derived state without useEffect, functional setState, lazy init, useDeferredValue, useTransition, useRef for transient values, memoized components, split hook computations, no inline components, effect event deps, event handler refs, initialize-once -- **react-composition-patterns.md** — Compound components, state lifting into providers, children over render props, explicit variants, context state/actions/meta interface, decoupled state management, React 19 ref-as-prop -- **react-view-transitions.md** — ViewTransition component API, activation triggers, CSS animation recipes, searchable grid pattern, card expand/collapse, type-safe helpers, persistent element isolation, troubleshooting -- **engineering-rules.md** — Output format, error handling, preferred patterns, anti-rationalization, hard boundaries, blocker criteria, systematic phases, death-loop prevention +- **engineering-rules.md** — Hard gate table and its exceptions, house preferences, anti-rationalization, verification STOP blocks, blocker criteria, death-loop limits, and the non-obvious failure-mode table (inline components, request-scoped state, `React.cache` argument equality, RSC serialization, effect remounts, localStorage throws) +- **react-view-transitions.md** — Canary `` activation rules, layout-vs-page placement, `key` semantics, text-morph and backdrop-filter CSS workarounds, duration budget, troubleshooting table +- **nextjs-security.md** — CVE-2025-29927 middleware bypass, CVE-2025-55182 Server Action closure params, GHSA-rvpw-p7vw-wj3m image-optimizer SSRF, RSC prop leakage, with detection commands See [shared-patterns/output-schemas.md](../skills/shared-patterns/output-schemas.md) for output format details. diff --git a/agents/typescript-frontend-engineer/references/engineering-rules.md b/agents/typescript-frontend-engineer/references/engineering-rules.md index 093b934c..deed6cd6 100644 --- a/agents/typescript-frontend-engineer/references/engineering-rules.md +++ b/agents/typescript-frontend-engineer/references/engineering-rules.md @@ -1,90 +1,9 @@ # TypeScript Frontend Engineer: Engineering Rules -Detailed engineering rules: output format, error handling, preferred patterns, -anti-rationalization, hard boundaries, blocker criteria, systematic phases, and -death loop prevention. +House rules, hard gates, and stop conditions. Generic TypeScript and React idioms are assumed known and are not restated here. --- -## Output Format - -This agent uses the **Implementation Schema**. - -### Before Implementation - -Requirements: [What needs to be built] -Type Safety Needs: [Where validation is needed] -React Patterns: [Which hooks/patterns apply] -Validation: [What external data needs Zod schemas] - - -### During Implementation -- Show TypeScript compiler output for errors -- Display Zod validation results -- Show test results if applicable - -### After Implementation -**Completed**: -- [Component/module implemented] -- [Types defined] -- [Validation added] -- [Tests passing (if applicable)] - -**Type Safety Checklist**: -- [ ] Strict mode enabled -- [ ] No `any` types (or justified) -- [ ] External data validated with Zod -- [ ] Return types explicit - -## Error Handling - -Common errors and their solutions. See [typescript-errors.md](typescript-errors.md) for comprehensive catalog. - -### Type Checking Too Slow -**Cause**: Complex types, circular references, or poor TypeScript configuration causing expensive type computations. -**Solution**: Enable incremental compilation (`"incremental": true`), use project references for monorepos, enable `skipLibCheck: true`, and profile with `tsc --diagnostics` to identify slow type computations. - -### Object is Possibly Null/Undefined -**Cause**: Strict null checking enabled (good!) but code doesn't handle potential null/undefined values. -**Solution**: Use optional chaining (`user?.name`), nullish coalescing (`user?.name ?? 'Unknown'`), type guards (`if (user) { ... }`), or validate with Zod before use. Prefer type guards over non-null assertions. - -### React 19: Ref Callback Return Type Mismatch -**Cause**: React 19 supports cleanup functions from ref callbacks, so TypeScript rejects implicit returns. -**Solution**: Use explicit function body for ref callbacks (`
{ myRef = el }} />`), or add cleanup function (`
{ myRef = el; return () => { myRef = null } }} />`). Prefer `useRef` hook for simple cases. - -## Preferred Patterns - -Patterns to follow. See [typescript-preferred-patterns.md](typescript-preferred-patterns.md) for full catalog. - -### Define Proper Types Instead of `any` -**Signal**: `const data: any = await fetch('/api/users')` -**Why this matters**: Defeats the purpose of TypeScript, loses autocomplete, allows runtime errors -**Preferred action**: Define proper types and validate with Zod: `const UserSchema = z.object({...}); const users = UserSchema.array().parse(data)` - -### Validate External Data with Zod -**Signal**: `return response.json() as User` (type assertion without validation) -**Why this matters**: API can return unexpected data, causes runtime errors, no protection against API changes -**Preferred action**: Always validate: `const data = await response.json(); return UserSchema.parse(data)` - -### Use Discriminated Unions for State -**Signal**: `interface State { data?: T; error?: string; loading?: boolean }` (allows invalid states) -**Why this matters**: Allows impossible states (loading + data + error), requires complex null checks, TypeScript can't narrow types -**Preferred action**: Use discriminated unions: `type State = { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: string }` - -## Anti-Rationalization - -See [shared-patterns/anti-rationalization-core.md](../../../skills/shared-patterns/anti-rationalization-core.md) for universal patterns. - -### Domain-Specific Rationalizations - -| Rationalization Attempt | Why It's Wrong | Required Action | -|------------------------|----------------|-----------------| -| "Type assertion is fine here, I know the shape" | Shape changes break at runtime, not compile time | Add Zod schema and validate | -| "`any` is just temporary for prototyping" | Technical debt spreads, types become unreliable | Use `unknown` or proper types immediately | -| "This API response is stable" | APIs change without notice | Always validate with Zod schema | -| "React 18 pattern still works" | Deprecated patterns removed in future versions | Migrate to React 19 patterns now | -| "Type checking is slow, I'll relax strict mode" | Loosening types defeats TypeScript's purpose | Optimize config, not type safety | - ## Hard Boundary Patterns (HARD GATE) Before writing TypeScript code, check for these patterns. If found: @@ -103,7 +22,6 @@ Before writing TypeScript code, check for these patterns. If found: ### Detection ```bash -# Find forbidden patterns grep -r ": any" src/ --include="*.ts" --include="*.tsx" grep -r "as User\|as.*Response" src/ --include="*.ts" --include="*.tsx" grep -r "@ts-ignore\|@ts-nocheck" src/ @@ -115,57 +33,29 @@ grep -r "useFormState" src/ --include="*.tsx" - `any` is acceptable ONLY with detailed comment explaining why (e.g., third-party library with no types) - Type assertions acceptable for DOM elements: `event.target as HTMLFormElement` - `forwardRef` acceptable only in React 18 projects not yet migrated +- Replace `@ts-ignore` with `@ts-expect-error` (it fails when the suppression stops being needed); link the GitHub issue if it is a TypeScript bug -## Blocker Criteria - -STOP and ask the user (always get explicit approval) before proceeding when: - -| Situation | Why Stop | Ask This | -|-----------|----------|----------| -| Multiple state management approaches possible | User preference (Zustand vs Redux vs Context) | "Use Zustand (lightweight), Redux Toolkit (complex apps), or Context (simple)?" | -| Unclear validation requirements | Over-validation hurts UX | "Validate on blur, on change, or on submit?" | -| API contract ambiguous | Wrong types cause runtime errors | "What's the exact API response structure? Can you share an example?" | -| React version unclear | React 18 vs 19 patterns differ | "Are you using React 18 or React 19?" | -| Breaking type changes | User coordination for migration | "This changes types used by 5 other components - proceed?" | -| Form library choice | Project consistency matters | "Use React Hook Form (recommended) or Formik?" | - -### Verify Before Assuming -- API response structure - always ask for example response -- Validation requirements - over-validation frustrates users -- Breaking type changes - affects other developers -- React version - patterns differ significantly - -## Systematic Phases +## House Preferences -For complex implementations (forms, API clients, state management): +- Create custom type utilities only after the pattern repeats 3+ times. Two occurrences is a coincidence. +- **interface** for object shapes, component props, class implementations. **type** for unions, intersections, tuples, mapped types, primitive aliases. Be consistent within a file. +- `safeParse` for user-facing validation (returns errors), `parse` for internal validation (throws). +- Always validate data crossing a trust boundary: APIs, user input, localStorage, URL params. -### Phase 1: UNDERSTAND -- [ ] Requirements clear (what needs to be built) -- [ ] External data sources identified (APIs, user input, localStorage) -- [ ] React version confirmed (18 vs 19) -- [ ] Type safety requirements defined - -Gate on checklist completion before proceeding. +## Anti-Rationalization -### Phase 2: PLAN -- [ ] Type interfaces designed -- [ ] Zod schemas defined for external data -- [ ] State management approach selected -- [ ] Component structure outlined +See [shared-patterns/anti-rationalization-core.md](../../../skills/shared-patterns/anti-rationalization-core.md) for universal patterns. -### Phase 3: IMPLEMENT -- [ ] Types and interfaces created -- [ ] Zod schemas implemented -- [ ] Components/hooks implemented -- [ ] Validation integrated +| Rationalization Attempt | Why It's Wrong | Required Action | +|------------------------|----------------|-----------------| +| "Type assertion is fine here, I know the shape" | Shape changes break at runtime, not compile time | Add Zod schema and validate | +| "`any` is just temporary for prototyping" | Technical debt spreads, types become unreliable | Use `unknown` or proper types immediately | +| "This API response is stable" | APIs change without notice | Always validate with Zod schema | +| "React 18 pattern still works" | Deprecated patterns removed in future versions | Migrate to React 19 patterns now | +| "Type checking is slow, I'll relax strict mode" | Loosening types defeats TypeScript's purpose | Optimize config, not type safety | -### Phase 4: VERIFY -- [ ] TypeScript compiles without errors -- [ ] No `any` types (or justified with comments) -- [ ] External data validated with Zod -- [ ] Tests passing (if applicable) +## Verification STOP Blocks -### Verification STOP Blocks These checkpoints are mandatory. Do not skip them even when confident. - **After writing code**: STOP. Run `npx tsc --noEmit` and show the output. Code that does not compile is not done. @@ -174,6 +64,19 @@ These checkpoints are mandatory. Do not skip them even when confident. - **Before editing a file**: Read the file first. Blind edits cause regressions. - **Before committing**: Do not commit to main. Create a feature branch. Main branch commits affect everyone. +## Blocker Criteria + +STOP and ask the user (always get explicit approval) before proceeding when: + +| Situation | Why Stop | Ask This | +|-----------|----------|----------| +| Multiple state management approaches possible | User preference (Zustand vs Redux vs Context) | "Use Zustand (lightweight), Redux Toolkit (complex apps), or Context (simple)?" | +| Unclear validation requirements | Over-validation hurts UX | "Validate on blur, on change, or on submit?" | +| API contract ambiguous | Wrong types cause runtime errors | "What's the exact API response structure? Can you share an example?" | +| React version unclear | React 18 vs 19 patterns differ | "Are you using React 18 or React 19?" | +| Breaking type changes | User coordination for migration | "This changes types used by 5 other components - proceed?" | +| Form library choice | Project consistency matters | "Use React Hook Form (recommended) or Formik?" | + ## Death Loop Prevention ### Retry Limits @@ -189,3 +92,25 @@ These checkpoints are mandatory. Do not skip them even when confident. 1. **Detection**: More than 3 type errors after attempting fix 2. **Intervention**: Simplify types - remove complex mapped types, use simpler interfaces 3. **Prevention**: Start with simple types, add complexity only when needed + +## Non-Obvious Failure Modes + +Symptoms that do not point at their cause. + +| Symptom | Cause | Fix | +|---|---|---| +| Input loses focus every keystroke; animations restart; effect cleanup+setup run every parent render; scroll resets | Component defined inline inside another component's body — new type identity each render | Hoist the component definition out | +| One user's data appears in another user's response | Module-level mutable variable in a server module is process-wide shared memory across concurrent renders | Keep request data local to the render tree | +| `React.cache()` never hits | Argument equality is `Object.is`; inline object args are new references each call | Pass primitives, not object literals | +| Both arrays serialize across the RSC boundary | `.toSorted()`/`.filter()`/`.map()`/`.slice()`/spread/`Object.assign()`/`structuredClone()` break reference-identity dedup | Move the transform to the client component | +| Effect re-runs after remount despite `[]` deps | Effects re-run on remount, including Strict Mode double-invocation in dev | Module-level guard for once-per-app-load work | +| `useEffectEvent` function in a dep array causes churn | Its identity is intentionally unstable by design | Never list it as a dependency | +| `localStorage` throws in production | `getItem`/`setItem` throw in private browsing (Safari, Firefox), over quota, or disabled by policy | Always wrap in try-catch | +| Custom swipe/zoom gesture cannot cancel scroll | `passive: true` forbids `preventDefault()` | Omit `passive` for listeners that must cancel default behavior | +| A single slow fetch blocks every sibling fetch from starting | Awaited sequentially instead of composed as parallel children | Compose parallel Suspense children | + +Ordering rule for Server Actions: validate input first (avoids auth overhead on malformed data), then authenticate, then authorize, then mutate. + +Do not hoist I/O to module level when assets vary per request or user, files may change at runtime, or large files would consume excessive memory. + +Safe at module level: immutable static config loaded once at startup, caches intentionally shared across requests and keyed correctly, process-wide singletons holding no user-specific mutable state. diff --git a/agents/typescript-frontend-engineer/references/nextjs-security.md b/agents/typescript-frontend-engineer/references/nextjs-security.md index 3d11d6b5..8765a29f 100644 --- a/agents/typescript-frontend-engineer/references/nextjs-security.md +++ b/agents/typescript-frontend-engineer/references/nextjs-security.md @@ -1,344 +1,49 @@ # Next.js / React Secure Implementation Patterns -Secure-by-default patterns for Next.js App Router and React applications. Each section shows what correct code looks like and why it matters. Load this reference when the task involves security, auth, XSS, CSRF, SSRF, Server Actions, middleware, or any vulnerability-related code. +Version-pinned Next.js security failures and the detection commands for each. Load when the task involves auth, XSS, CSRF, SSRF, Server Actions, middleware, or the image optimizer. ---- - -## Verify Auth in Every Server Action - -Every Server Action must verify the session independently. Page-level auth checks do not extend to Server Actions defined within the page. Server Actions are POST endpoints that any HTTP client can invoke directly. - -```ts -// Correct: auth verified inside the action -'use server'; -import { auth } from '@/auth'; - -export async function deleteUser(userId: string) { - const session = await auth(); - if (!session?.user?.isAdmin) { - throw new Error('unauthorized'); - } - await db.user.delete({ where: { id: userId } }); -} - -// Correct: inline Server Action with ownership re-verification -async function cancelOrder() { - 'use server'; - const session = await auth(); - // Re-verify ownership — closure-captured params.id is attacker-controllable - const order = await db.order.findFirst({ - where: { id: params.id, userId: session.userId }, - }); - if (!order) throw new Error('unauthorized'); - await db.order.update({ where: { id: order.id }, data: { status: 'cancelled' } }); -} -``` - -**Why this matters**: Next.js Server Actions are RPC endpoints. An attacker who knows the action's exported name can invoke it with any arguments without visiting the page. Page-level `redirect('/login')` does nothing for the action. Inline actions that capture page `params` in their closure look scoped but the captured values are serialized into action metadata and are attacker-controllable. CVE-2025-55182 documents this class. - -**Detection**: -```bash -rg -n "'use server'" . -rg -A5 "'use server'" . | rg -v 'auth\(\)|getSession\(\)|getServerSession' -``` +Generic advice (escape output, validate input, use HTTPS) is assumed known. What follows is the framework-specific part that gets missed. --- -## Use Middleware as Defense-in-Depth, Not Sole Auth Layer +## Server Actions Are Public RPC Endpoints -Implement auth checks in both middleware and in each route handler or Server Action. Middleware alone is bypassable. +Every Server Action must verify the session independently. Page-level auth does not extend to Server Actions defined within that page. -```ts -// middleware.ts — defense-in-depth, not sole protection -import { auth } from '@/auth'; -import { NextResponse } from 'next/server'; -import type { NextRequest } from 'next/server'; +An attacker who knows the action's exported name can invoke it with any arguments without ever visiting the page. Page-level `redirect('/login')` does nothing for the action. Inline actions that capture page `params` in their closure look scoped, but the captured values are serialized into action metadata and are attacker-controllable — **re-verify ownership inside the action**. CVE-2025-55182 documents this class. -export async function middleware(req: NextRequest) { - const session = await auth(); - if (!session && req.nextUrl.pathname.startsWith('/dashboard')) { - return NextResponse.redirect(new URL('/login', req.url)); - } - return NextResponse.next(); -} +Order: validate input, then authenticate, then authorize, then mutate. -export const config = { - // Cover both pages and API routes - matcher: ['/dashboard/:path*', '/api/dashboard/:path*'], -}; -``` - -```ts -// app/api/dashboard/settings/route.ts — re-verify auth in the handler -import { auth } from '@/auth'; - -export async function GET() { - const session = await auth(); - if (!session) return new Response('unauthorized', { status: 401 }); - // ...proceed with verified session -} -``` - -**Why this matters**: CVE-2025-29927 allowed bypassing Next.js middleware entirely by sending the `x-middleware-subrequest` header. Fixed in Next.js 15.2.3, but the lesson stands: middleware is a single enforcement surface. Route handlers and Server Actions are independent surfaces that need their own checks. Middleware matcher patterns can also miss paths (`/admin` vs `/admin/`, `/api/admin/*` not covered by `/admin/:path*`). - -**Detection**: ```bash -rg -n 'matcher' middleware.ts -find . -path '*/app/api/*/route.ts' -o -path '*/app/api/*/route.tsx' | xargs rg -L 'auth\(\)|getSession' -jq '.dependencies.next' package.json +# Server Actions with no session check +rg -A5 "'use server'" . | rg -v 'auth\(\)|getSession\(\)|getServerSession' ``` ---- - -## Protect API Route Handlers With Auth Checks - -Every `app/api/**/route.ts` handler must verify authentication. Route handlers are HTTP endpoints accessible to any client. +## Middleware Is Not a Security Boundary -```ts -// app/api/invoices/route.ts -import { auth } from '@/auth'; +CVE-2025-29927 allowed bypassing Next.js middleware entirely by sending the `x-middleware-subrequest` header. Fixed in Next.js 15.2.3, but the lesson stands: middleware is a single enforcement surface. Route handlers and Server Actions are independent surfaces that need their own checks. -export async function GET() { - const session = await auth(); - if (!session) { - return Response.json({ error: 'unauthorized' }, { status: 401 }); - } +Middleware matcher patterns also silently miss paths: `/admin` vs `/admin/`, and `/api/admin/*` is not covered by `/admin/:path*`. - // Scope queries to the authenticated user - const invoices = await db.invoice.findMany({ - where: { userId: session.user.id }, - select: { id: true, amount: true, status: true, createdAt: true }, - }); - return Response.json(invoices); -} -``` - -**Why this matters**: Route handlers are independent enforcement surfaces. Even if the corresponding page has auth in `getServerSideProps` or the page component, the API route is a separate endpoint. A page that scopes data by `session.userId` provides no protection when the route handler does `db.invoice.findMany()` without scoping. - -**Detection**: ```bash +# Route handlers with no auth find . -path '*/app/api/*/route.ts' -o -path '*/app/api/*/route.tsx' | xargs rg -L 'auth\(\)|getSession' +jq '.dependencies.next' package.json # confirm >= 15.2.3 ``` ---- - -## Select Explicit Fields at the RSC Boundary - -When passing data from Server Components to Client Components, select only the fields the UI needs. Full database rows are serialized into the HTML response. - -```tsx -// Correct: select specific fields at the query level -export default async function Profile() { - const session = await auth(); - const user = await db.user.findUnique({ - where: { id: session.userId }, - select: { id: true, displayName: true, avatarUrl: true }, - }); - return ; -} - -// Correct: use a DTO type to enforce the boundary -type UserPublic = { - id: string; - displayName: string; - avatarUrl: string | null; -}; - -function ProfileView({ user }: { user: UserPublic }) { - return
{user.displayName}
; -} -``` - -**Why this matters**: React Server Components serialize all props into the HTML page source. A full database row includes password hashes, API tokens, internal flags, and any column added by future migrations. Even fields not rendered in the UI are visible in the page source and network response. - -**Detection**: -```bash -rg -n 'findUnique\(|findFirst\(|findMany\(' . --type ts --type tsx | rg -v 'select' -rg -n 'getServerSideProps' . --type ts --type tsx | rg -v 'select' -``` - ---- - -## Configure Content Security Policy - -Set a Content Security Policy that restricts script and resource sources. Next.js supports CSP via `next.config.js` headers or middleware. - -```ts -// next.config.js -const securityHeaders = [ - { - key: 'Content-Security-Policy', - value: [ - "default-src 'self'", - "script-src 'self' 'unsafe-eval'", // Required for Next.js dev; remove unsafe-eval in prod - "style-src 'self' 'unsafe-inline'", - "img-src 'self' https://cdn.example.com data:", - "connect-src 'self' https://api.example.com", - "frame-ancestors 'none'", - ].join('; '), - }, - { key: 'X-Frame-Options', value: 'DENY' }, - { key: 'X-Content-Type-Options', value: 'nosniff' }, - { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }, - { key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' }, -]; - -module.exports = { - async headers() { - return [{ source: '/:path*', headers: securityHeaders }]; - }, -}; -``` - -**Why this matters**: CSP blocks inline script injection (XSS), limits where resources can be loaded from, and prevents clickjacking via `frame-ancestors`. Without CSP, a single XSS vector gives full control of the page. +## The Image Optimizer Is an Open Proxy by Default -**Detection**: -```bash -rg -n 'Content-Security-Policy|contentSecurityPolicy' . -rg -n 'X-Frame-Options|frame-ancestors' . -``` - ---- - -## Render Content Through React's Built-in Escaping - -Use React's default JSX escaping for all user-facing content. When raw HTML rendering is genuinely needed (CMS content, markdown output), sanitize with DOMPurify or a server-side sanitizer first. +`remotePatterns: [{ hostname: '**' }]` turns `/_next/image` into an SSRF primitive. `/_next/image?url=http://169.254.169.254/latest/meta-data/` exfiltrates cloud instance metadata through the image endpoint (GHSA-rvpw-p7vw-wj3m). Enumerate allowed hostnames explicitly. -```tsx -// Correct: React JSX auto-escapes by default -function Comment({ text }: { text: string }) { - return

{text}

; // "