sota-web-frameworks
State-of-the-art engineering rules (2026) for the JavaScript SSR meta-frameworks: React 19 + Next.js (App Router, React Server Components, Server Actions) and Vue 3 + Nuxt 4 (Nitro server routes, composables) — plus the cross-cutting concerns of server rendering: hydration correc
Install
npx skills add https://github.com/martinholovsky/SOTA-skills/tree/main/skills/sota-web-frameworks
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install martinholovsky-sota-skills@llmmart
git clone https://github.com/martinholovsky/SOTA-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole martinholovsky/sota-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
SOTA Web Frameworks (2026)
Purpose
This skill encodes the 2026 state of the art for the two dominant JavaScript SSR
stacks — React + Next.js and Vue + Nuxt — and the server-rendering concerns
they share. It is deliberately framework-specific: the traps that matter here
(the RSC server/client boundary, Server Actions as public endpoints, hydration
mismatches, SSR state leaking across requests, runtimeConfig/NEXT_PUBLIC_
secret boundaries) do not exist at the plain-language level.
Two modes:
- BUILD — writing or modifying components, routes, and data-fetching to this standard.
- AUDIT — reviewing an existing app and reporting findings.
Read SKILL.md fully; load rules/*.md on demand per the index below. Verify every
version/CVE claim at use time — this file's facts were primary-sourced 2026-07 but
framework security moves weekly (see the 2025-12 React Server Components RCE).
Scope boundaries (what lives elsewhere)
This skill stacks on top of the general skills — load them together, don't duplicate:
sota-javascript-typescript— stricttsconfig, type design, promises, Node hardening, npm supply chain. The language; this skill is the framework.sota-frontend-design— visual design, layout, components-as-UX, WCAG 2.2 accessibility, motion. This skill covers component engineering, not design.sota-code-security— generic XSS/CSRF/SSRF/authn/authz/crypto theory. This skill covers the framework-specific expression of those (RSC data exposure, Server Action authz,v-html, next/image SSRF).sota-performancerules/06 — Core Web Vitals, bundle budgets, image/font loading. This skill covers render-strategy choice (SSR/SSG/ISR/PPR) and hydration.sota-api-design— REST/GraphQL contract design for the routes themselves.
BUILD mode
- Establish context first. Read
package.jsonfor the exact React/Next or Vue/Nuxt majors and the render mode in use (App vs Pages Router; Nuxt SSR vsssr: false). Match the project's baseline — no Server Actions on a Pages-Router app, nodefineModelbelow Vue 3.4. Confirm the versions are supported and patched against the CVE tables inrules/03/rules/05. (rules/01) - Default to the current idiom: React function components + hooks (let the React
Compiler memoize — don't hand-write
useMemoeverywhere); Vue Composition API with<script setup>. Server Components by default in Next App Router,"use client"only at the leaves that need interactivity. (rules/02,rules/04) - The server/client boundary is a security boundary, not just a perf one. Every
prop crossing server→client is serialized into the HTML/RSC payload and is public.
Authorization lives next to the data (a Data Access Layer / validated server
route), never only in middleware or a layout. (
rules/03,rules/07) - Treat every Server Action and Nitro route as a public, unauthenticated HTTP
endpoint until it validates input and checks authz itself — even if it looks
internal or is never imported. (
rules/03,rules/05,rules/07) - Get hydration right by construction: deterministic render (no
Date.now(),Math.random(), orwindowin render), stable IDs viauseId, SSR-safe shared state (useState/per-request instances, never module-level refs). (rules/06) - Serialize SSR state safely (framework serializer or escaped JSON, never naked
JSON.stringifyinto<script>) and wire CSP (nonce or hash) knowing it forces dynamic rendering in Next. (rules/06,rules/07) - Tests accompany code (
sota-testing): component tests plus at least one test that exercises the server/client boundary or a server route's authz.
AUDIT mode
- Fingerprint + patch-check first. Pin exact framework/loader versions from the
lockfile and diff them against the CVE tables in
rules/03andrules/05. An unpatched React2Shell (CVE-2025-55182) or middleware bypass (CVE-2025-29927) is CRITICAL on its own, before any code is read. - Trace the trust boundary. Grep
"use client"/"use server",defineProps,runtimeConfig,NEXT_PUBLIC_/NUXT_PUBLIC_; find where server data crosses to the client and where authz is enforced. Middleware/layout-only authz is a finding. - Run each relevant rules file's Audit checklist (grep-driven), then read for design: hydration determinism, SSR state isolation, caching of personalized pages.
- Verify every finding — a
v-htmlfed a constant is not XSS; a Server Action that re-checks the session is not IDOR. Note mitigations already present.
Severity conventions
| Severity | Meaning | Examples |
|---|---|---|
| CRITICAL | Exploitable now / RCE / data loss | Unpatched RSC deserialization RCE; middleware-bypass auth on the only authz layer; secret in NEXT_PUBLIC_/public runtimeConfig; unserialize-class sink |
| HIGH | Exploitable with preconditions | Server Action / Nitro route with no authz (IDOR); SSRF via next/image ** or user-URL fetch; dangerouslySetInnerHTML/v-html of user data; whole-DB-row as a client prop |
| MEDIUM | Real but bounded, or reliability | Personalized SSR page cacheable at a shared CDN; hydration mismatch on user data; missing CSP; module-level SSR state; unpatched non-critical CVE |
| LOW | Deviation from SOTA | Pages Router for greenfield; hand-rolled memoization vs React Compiler; $fetch double-fetch in setup; Options API for a new app |
| INFO | Worth knowing | Newer render mode available; migration opportunities |
Finding format
file:line | rule violated (rules/NN §S) | severity | effort | fix
Effort: trivial · small · medium · large. Group by severity, CRITICAL first. Borderline severities state the deciding assumption; unconfirmed findings are marked "needs verification", never asserted. End with per-severity counts, the sweep commands run, and explicit "checked and clean" areas.
Rules index
| File | Read this when... |
|---|---|
rules/01-baseline.md |
choosing or verifying a stack and version floor (React/Next/Vue/Nuxt support+EOL matrix), picking a render mode (CSR/SSR/SSG/ISR/PPR), project setup, React Compiler |
rules/02-react.md |
writing React components: hooks rules, useId/useEffect/refs, Suspense & error boundaries, use()/Actions/useActionState, memoization & the React Compiler, dangerouslySetInnerHTML |
rules/03-nextjs.md |
Next.js App Router: Server vs Client Components, Server Actions, the caching model (use cache/Cache Components/PPR/ISR/revalidate), proxy.ts/middleware, the Data Access Layer, and Next CVEs |
rules/04-vue.md |
writing Vue: Composition API & <script setup>, reactivity pitfalls (props destructure, shallowRef, watchers, effectScope), defineModel, TypeScript, and Vue XSS (v-html, template injection) |
rules/05-nuxt.md |
Nuxt 4: data fetching (useFetch/useAsyncData/$fetch), useState, runtimeConfig, Nitro server routes & auth, routeRules/hybrid rendering, islands, and Nuxt/Nitro/h3/IPX CVEs |
rules/06-ssr-hydration.md |
anything SSR: hydration mismatches & determinism, SSR state-serialization XSS, cross-request state pollution, caching personalized pages safely, CSP with streaming SSR |
rules/07-security.md |
a security pass: the server/client secret boundary, authorization placement (post-CVE-2025-29927), SSRF surfaces, the consolidated framework CVE reference, and supply-chain notes |
Top-10 non-negotiables
- Run supported, patched majors. React ≥ 19.x, Next ≥ 15.x (16.x current), Vue
≥ 3.5, Nuxt ≥ 4.x (Nuxt 3 security-only until 2026-07-31). Cross-check the CVE
tables — an unpatched RSC RCE (CVE-2025-55182) or middleware bypass
(CVE-2025-29927) is a ship-stopper. (
rules/01,rules/03,rules/05) - Authorization lives next to the data, never only in middleware or a layout.
proxy.ts/middleware is an optimization, not the auth layer; each Server Action, Route Handler, and Nitro route re-checks authn + authz. (rules/03,rules/07) - Every prop that crosses server→client is public. It's serialized into the RSC
payload / HTML. Pass minimal DTOs, never raw DB rows, tokens, or
process.env. (rules/03,rules/07) - Server Components by default;
"use client"only where interactivity requires it. Keep secrets and data access in Server Components / server-only modules (import 'server-only'). (rules/02,rules/03) - Secrets never reach the client bundle. Nothing sensitive in
NEXT_PUBLIC_*,VITE_*, orruntimeConfig.public— those are inlined at build time. (rules/07) - Validate every server-route/action input with a schema (
getValidatedBody/Zod; never trustformData,searchParams, or headers), and check resource ownership to prevent IDOR. (rules/03,rules/05) - Hydrate deterministically: no
Date/random/windowin render, stable IDs viauseId, SSR-safe shared state — and never "fix" a mismatch by injecting server-side user HTML. (rules/06) - Serialize SSR state safely. Framework serializer (devalue) or
<-escaped JSON in<script>; nakedJSON.stringifyinto a script tag is XSS. (rules/06) - User HTML is dangerous HTML.
dangerouslySetInnerHTML/v-htmlonly on sanitizer output; validatehref/URL schemes (javascript:); locknext/imageremotePatternsto explicit hosts (no**). (rules/02,rules/04,rules/07) - Don't cache personalized SSR at a shared cache.
Cache-Control: privatefor per-user pages; know that CDNs dropVary; wire CSP nonces knowing they force dynamic rendering. (rules/06)
Files (sota-skills)
-
rules
-
01-baseline.md 6.8 KB
# 01 — Baseline: versions, support windows, render modes Fast-moving facts. Every version and EOL date below was primary-sourced 2026-07; **re-verify at use time** before pinning — these stacks ship majors yearly and security releases weekly. ## 1. Supported versions and EOL (verify before pinning) | Runtime | Current stable (2026-07) | Floor for new code | EOL / support note | |---|---|---|---| | React | 19.2.x | 19.x | 18.x maintained but no new features; React Compiler needs 19 idioms | | Next.js | 16.2.x | 15.x (16.x preferred) | **Active LTS 16.x**, **Maintenance LTS 15.x** (critical + security ~2y from 2024-10-21). Everything **< 15 is unsupported** — treat as a finding | | Vue | 3.5.x | 3.5+ | Vue 2 **EOL 2023-12-31** (paid extended support only). 3.6 (Vapor mode) in beta — not stable | | Nuxt | 4.4.x | 4.x | Nuxt 3 **security-patches-only until 2026-07-31** (then EOL); Nuxt 2 EOL 2024-06-30; Nuxt 5 (unreleased) brings Nitro v3 + h3 v2 | | Nitro | 2.13.x | 2.x | v3 in beta; ships with Nuxt 5 | | Pinia | 3.x | 3.x | Pinia 3 dropped Vue 2; default Nuxt/Vue store | Sources: react.dev/versions, nextjs.org/support-policy, github.com/vuejs/core releases, nuxt.com/docs/4.x/community/roadmap. Running an EOL major (Vue 2, Nuxt 2, Next < 15) means no security patches — HIGH at minimum for an internet-facing app. - **React Compiler 1.0 is stable** (2025-10-07): a build-time plugin that auto-memoizes, removing most manual `useMemo`/`useCallback`/`memo`. It requires Rules-of-Hooks-clean code (see `rules/02`). Opt-in for Next/Vite today; verify current adoption status for your toolchain. Prefer it over hand-memoization for new code, but it is not a substitute for fixing render-model mistakes. ## 2. Choosing a stack (framework selection) Both stacks are mature and SSR-first; the choice is usually ecosystem/team, not capability. Neutral guidance, not prescription: - **React + Next.js** — largest ecosystem; React Server Components + Server Actions are the reference implementation of the RSC model; the deepest hosting integration (Vercel and others). Cost: the RSC/client mental model is genuinely hard, and the security surface is large and fast-moving (see the 2025-12 RSC RCE). - **Vue + Nuxt** — gentler learning curve, batteries-included conventions (file-based routing, auto-imports, `runtimeConfig`), Nitro gives a portable server runtime. Smaller but healthy ecosystem. - **Not every app needs a meta-framework.** A purely client-side app (internal dashboard behind auth, no SEO need) can be a plain Vite SPA — you shed the entire SSR/hydration/RSC attack surface. Reach for Next/Nuxt when you need SSR/SSG for SEO, fast first paint, or server-side data access. Don't adopt SSR for its own sake. Record the decision as an ADR (`sota-docs-workflow`); it drives everything downstream. ## 3. Render modes — pick per route, not per app The single most consequential design choice. Modern frameworks let you mix modes per route, so match each route to its data: | Mode | What it is | Use for | Watch out for | |---|---|---|---| | **CSR** (client only) | JS renders in the browser; empty initial HTML | Highly interactive, auth-gated, no-SEO views (`ssr: false` route in Nuxt) | Blank first paint; not indexable | | **SSR** (per request) | HTML rendered on each request | Personalized/authenticated pages, fresh data | Server cost; **must not be cached at a shared CDN if personalized** (`rules/06`) | | **SSG / prerender** | HTML rendered at build | Marketing, docs, anything static | Rebuild to update; no per-user content | | **ISR / SWR** | Static + periodic/on-demand revalidation | Semi-static content (catalogs, blogs) | Stale windows; cache-invalidation correctness | | **PPR** (Next, Cache Components) | Static shell + streamed dynamic holes via Suspense | Pages mixing static chrome + dynamic data | Uncached data outside `<Suspense>` is a build error; incompatible with CSP nonces | - **Next.js**: route-segment config + `"use cache"`; PPR is the default when `cacheComponents: true`. Fetch is **not cached by default since v15** — opt in explicitly (`rules/03`). - **Nuxt**: `routeRules` in `nuxt.config` sets per-route mode (`ssr: false`, `prerender: true`, `swr: <ttl>`, `isr: <ttl>`) — hybrid rendering (`rules/05`). - **Security corollary:** the more a route is cached and shared, the more a caching bug leaks one user's data to another. Personalized ⇒ SSR + `private` cache. Static ⇒ safe to cache widely. Decide this consciously per route. ## 4. Project setup baseline - **TypeScript strict** — non-negotiable; details in `sota-javascript-typescript` rules/01. Frameworks generate a `tsconfig`; extend, don't loosen it. - **Lockfile committed**, exact framework versions pinned, Dependabot/Renovate on — framework CVEs are frequent and the fix is almost always "upgrade" (`rules/07`, `sota-devsecops`). - **Lint the framework rules**: `eslint-plugin-react-hooks` (Rules of Hooks — also what the React Compiler needs) / `eslint-plugin-vue`; Next's and Nuxt's own ESLint configs. A hooks-rule violation is a real bug, not style. - **Env discipline from day one**: server secrets in unprefixed env vars; only deliberately-public config in `NEXT_PUBLIC_*` / `runtimeConfig.public` / `VITE_*` (`rules/07`). Never commit `.env`. - **CI gates**: typecheck, lint, tests, `npm audit`/`osv-scanner`, and a build. Add a bundle-size check if shipping to the browser (`sota-performance` rules/06). ## Audit checklist ```bash # Framework majors in use — compare against the support table above grep -E '"(react|react-dom|next|vue|nuxt|nitropack|pinia)"' package.json cat package.json | grep -A2 '"dependencies"' # then read lockfile for exact patch # EOL / unsupported runtimes (findings) node -e "const p=require('./package.json');const d={...p.dependencies,...p.devDependencies};for(const k of ['vue','nuxt','next'])if(d[k])console.log(k,d[k])" # vue ^2 -> EOL; nuxt ^2 -> EOL; next <15 -> unsupported # Render-mode inventory grep -rn 'ssr:\s*false\|routeRules\|prerender\|export const dynamic\|cacheComponents' nuxt.config.* next.config.* app/ pages/ 2>/dev/null # Hooks/vue lint present? grep -rn 'react-hooks\|eslint-plugin-vue\|next/core-web-vitals\|@nuxt/eslint' .eslintrc* eslint.config.* package.json 2>/dev/null ``` - [ ] Every framework major is supported and receiving security patches (no Vue 2, Nuxt 2, Next < 15)? - [ ] Exact versions pinned + lockfile committed + automated dependency updates on? - [ ] Render mode chosen per route to match its data (personalized ⇒ SSR + private cache)? - [ ] TypeScript strict; hooks/vue lint rules enforced in CI? - [ ] Public-env boundary understood: no secrets in `NEXT_PUBLIC_`/`public`/`VITE_`? - [ ] For a no-SEO auth-gated app: is a meta-framework actually needed, or would a plain SPA shed the SSR attack surface? -
02-react.md 7.5 KB
# 02 — React 19: components, hooks, Actions, memoization Scope: React itself (any renderer). Next-specific RSC/Server-Action mechanics are in `rules/03`; hydration in `rules/06`. TypeScript-with-React lives in `sota-javascript-typescript`. ## 1. Component and hook fundamentals - **Function components + hooks only.** Class components are legacy; no new ones. - **Rules of Hooks are load-bearing, not style.** Call hooks unconditionally, at the top level, in the same order every render. Violations are real bugs *and* they break the React Compiler (which refuses to optimize non-conforming components). Enforce with `eslint-plugin-react-hooks`. - **`useEffect` is for synchronizing with external systems**, not for deriving data. The react.dev guide "You Might Not Need an Effect" is the reference: compute derived values during render, adjust state during render (not in an effect), fetch via a framework loader or a data library — not a bare `useEffect(fetch)`, which races and double-runs. An effect that only sets state from props/state is a smell. - **Every effect that subscribes must clean up** (return a teardown): listeners, timers, sockets, subscriptions. A missing cleanup leaks across Strict Mode's double-invoke and across remounts. - **Stable IDs come from `useId`**, never `Math.random()` or a module counter — this is what keeps SSR and client markup matching (`rules/06`). For form-field `id`/ `htmlFor` pairs and ARIA wiring. - **Refs (`useRef`) are escape hatches**: DOM access and mutable non-render values. Don't read/write `ref.current` during render. Prefer state for anything that should trigger a re-render. ## 2. Suspense, transitions, and error boundaries - **Suspense** declares loading UI for async children (lazy components, RSC/data that suspends). Pair every dynamic boundary with a fallback; in Next PPR, uncached data *must* sit inside a `<Suspense>` or the build fails (`rules/03`). - **`useTransition` / `startTransition`** mark state updates as non-urgent so input stays responsive; `useDeferredValue` defers re-rendering expensive subtrees. Reach for these on measured jank, not preemptively. - **Error boundaries are mandatory around risky subtrees** (lazy loads, third-party widgets, RSC islands). Hooks can't catch render errors — you still need a class boundary or `react-error-boundary`. Server-render errors and hydration errors surface here too. Don't render user-controlled error text as HTML. ## 3. The Actions model (React 19) React 19 stabilized "Actions" — async functions wired into transitions with built-in pending/error/optimistic state. - **`useActionState(action, initial)`** — wraps an async action, returns `[state, formAction, isPending]`; drive a `<form action={formAction}>`. Replaces hand-rolled `isLoading`/`error` state around form submits. - **`useOptimistic`** — show an optimistic value while the action is in flight, auto-reverting on failure. - **`use(promise)` / `use(context)`** — unwrap a promise (suspends) or read context, and unlike other hooks `use` may be called conditionally. Feed it a *cached/stable* promise (from an RSC or a cache), never a promise created inline in render — that creates a new promise every render and never resolves. - **`ref` is a regular prop** in React 19 (no more `forwardRef` for new code); `ref` cleanup callbacks are supported. - These integrate with Next Server Actions (`rules/03`): `formAction` can be a `"use server"` function. The client-side pending/optimistic UX is React; the security of the action is server-side and covered in `rules/03`/`rules/07`. ## 4. Memoization and the React Compiler - **Prefer the React Compiler over manual memoization.** Compiler 1.0 (stable, 2025-10) auto-memoizes components and values, making most `useMemo`, `useCallback`, and `React.memo` unnecessary — *if* your code follows the Rules of Hooks and treats props/state as immutable. Don't rip out existing memoization blindly, but stop hand-writing new memo wrappers as a reflex. - **Where you still memoize by hand** (no compiler, or a proven hot path): memoize the *expensive* computation or a referentially-stable callback passed to a memoized child. Memoizing a cheap value costs more than it saves. Measure (`sota-performance`). - **Never mutate state or props.** Compiler correctness and React's rendering both assume immutability; mutation causes stale UI and defeats memoization. Use immutable updates (spread, `.map`, or a library). - **Keys must be stable and unique** across siblings — never the array index for reorderable/insertable lists (causes state to attach to the wrong row and subtle data-corruption bugs). Use a domain ID. ## 5. React-specific XSS: the injection sinks Generic XSS theory is in `sota-code-security` rules/05; here are the React sinks. - **`dangerouslySetInnerHTML` is the one HTML-injection sink** JSX gives you (JSX text is auto-escaped). Only ever feed it sanitizer output (DOMPurify or a server sanitizer with an allowlist), never raw user/CMS HTML. ```jsx // BAD — HIGH: stored/reflected XSS <div dangerouslySetInnerHTML={{ __html: comment.body }} /> // GOOD — sanitize first (allowlist), or don't use HTML at all import DOMPurify from 'dompurify'; <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(comment.body) }} /> ``` - **URL/`href` injection:** `<a href={userUrl}>` allows `javascript:` and `data:` schemes. Validate the scheme (`https?:`/relative) before rendering; the same applies to `src`, `formAction`, and `<img src>`. - **Spreading unknown props** (`<div {...userControlledObject}>`) can inject `dangerouslySetInnerHTML` or event handlers — never spread attacker-influenced objects onto DOM elements. - **Rendering into a portal / third-party DOM** you don't control inherits that DOM's trust; don't mount React onto server-rendered nodes that contain user HTML (mirrors the Vue rule in `rules/04`; hydration-XSS angle in `rules/06`). - CSP is defense-in-depth, not a substitute (`rules/06`, `rules/07`). ## Audit checklist ```bash # HTML injection sink — every hit needs a sanitizer on the source grep -rn 'dangerouslySetInnerHTML' --include='*.tsx' --include='*.jsx' src app components # javascript:/data: URL sinks grep -rnE 'href=\{|src=\{|formAction=\{' --include='*.tsx' src app | grep -iv 'sanitiz' # Effect smells: fetch-in-effect, setState-only effects, missing deps grep -rnE 'useEffect\(' --include='*.tsx' src app | head grep -rn 'Math.random()\|Date.now()' --include='*.tsx' src app # in render => hydration bug (rules/06) # Rules of Hooks / compiler-blocking violations rely on lint: grep -rn 'react-hooks' .eslintrc* eslint.config.* package.json # Legacy patterns grep -rn 'forwardRef\|class .* extends .*Component' --include='*.tsx' src app # forwardRef unneeded in 19 grep -rnE 'key=\{.*index' --include='*.tsx' src app # index-as-key on dynamic lists ``` - [ ] Every `dangerouslySetInnerHTML` fed sanitizer output (allowlist), not raw user/CMS HTML? - [ ] `href`/`src`/URL props validated against a scheme allowlist (no `javascript:`/`data:`)? - [ ] No `Math.random()`/`Date.now()`/`window` used *during render* (hydration determinism)? - [ ] Effects only for external synchronization, each with cleanup; no fetch-in-bare-effect racing? - [ ] Stable domain IDs as list keys (not array index) for insertable/reorderable lists? - [ ] Rules of Hooks enforced by lint (also required for the React Compiler)? - [ ] Error boundaries around lazy/third-party/RSC subtrees; error text not rendered as HTML? -
03-nextjs.md 11.8 KB
# 03 — Next.js: App Router, Server Actions, caching, CVEs Baseline: Next.js 16.x (App Router). Pages Router still ships but is not the recommended model for new code. Hydration is in `rules/06`; the consolidated security boundary and CVE reference in `rules/07`. **Re-verify every CVE range at use time** — the version numbers below were primary-sourced 2026-07. ## 1. Server vs Client Components — the boundary is everything App Router components are **Server Components by default**. They run only on the server, can be `async`, and can touch the database, filesystem, and secrets directly. - **`"use client"` marks the boundary**, not a single component — everything imported into a `"use client"` module becomes client code. Push the directive to the leaves that actually need interactivity (state, effects, event handlers, browser APIs). - **Everything a Server Component passes as a prop to a Client Component is serialized into the RSC payload and shipped to the browser** — it is public. The canonical anti-pattern is passing a whole DB row (with password hashes, internal flags) when the client needs two fields. Pass minimal DTOs. - **Keep secrets and data access server-side.** Mark server-only modules with `import 'server-only'` so importing them from a Client Component is a *build error*. Only server code should read `process.env` secrets. - **Most** functions and class instances can't cross the boundary; React throws. That's a feature — it stops you leaking server closures to the client. Know the exceptions before you report one as a violation: React's serializable list **does** include `Date`, `Map`, `Set`, `TypedArray`, `ArrayBuffer`, promises, JSX elements, and **functions that are Server Functions** (`'use server'`). What is not serializable is a function *not* exported from a client-marked module or marked `'use server'`, a class, an instance of any non-built-in class, a null-prototype object, and a non-global symbol ([React: serializable types](https://react.dev/reference/rsc/use-client), verified 2026-09-16). The minimal-DTO advice below stands on its own security merits. ## 2. Server Actions — public endpoints with ergonomic syntax A `"use server"` function is compiled into a **public HTTP POST endpoint**. This is the highest-value Next security topic. ```tsx 'use server'; export async function deletePost(id: string) { const user = await requireUser(); // 1. authenticate — every time const post = await db.post.find(id); if (post.authorId !== user.id) throw new Error('forbidden'); // 2. authorize (ownership) const parsed = z.string().uuid().parse(id); // 3. validate input await db.post.delete(parsed); } ``` - **Every action re-checks authn + authz + input**, even if it's never imported into a page and even if it "looks internal." Next's dead-code elimination and encrypted, per-build action IDs raise the bar but the docs are explicit: treat every action as externally reachable. Missing authz here is IDOR (HIGH). - **Validate all input with a schema** (Zod/Valibot). `formData`, arguments, and any reflected header are attacker-controlled. Never trust a hidden field like `isAdmin`. - **Return values are serialized to the client** — filter them the same as a prop (don't return the raw row). - **CSRF:** Next checks Origin vs Host for actions; behind a proxy set `experimental.serverActions.allowedOrigins`. Closure-captured variables are encrypted per build (key `NEXT_SERVER_ACTIONS_ENCRYPTION_KEY` — set it explicitly for multi-replica deploys so restarts don't invalidate in-flight forms); `.bind()` arguments are **not** encrypted. Don't rely on encryption for authz. - **Route Handlers** (`route.ts`) are the same story — treat every one as a public API endpoint (`sota-api-design`, `sota-code-security`). ## 3. Authorization placement — the Data Access Layer Post-CVE-2025-29927 (middleware bypass, below), the official guidance is unambiguous: **do not put your only authorization check in middleware or a layout.** - **`proxy.ts`/middleware** (renamed from `middleware.ts` in Next 16; `proxy.ts` defaults to the Node runtime) is for coarse, optimistic checks (redirect if no session cookie) — an optimization, not the security boundary. It can be bypassed (CVE-2025-29927) and its matcher can silently stop covering a route. - **Layouts don't re-render on client navigation**, so a layout-level auth check is not re-evaluated as the user navigates — insufficient on its own. - **Data Access Layer (DAL):** a `server-only` module that every read/write goes through, which authenticates, authorizes, and returns minimal DTOs. Authorization lives *next to the data*. This is the pattern the Next docs recommend for new apps. - **Taint APIs** (`experimental_taintObjectReference`/`taintUniqueValue`) are still experimental (`experimental.taint`) — a backstop that throws if a tainted object reaches the client, not a primary control (cloning/deriving escapes the taint). ## 4. The caching model (know what's cached, and when) The caching model changed materially; stale mental models cause both bugs and leaks. - **`fetch` is not cached by default since v15.** Opt in per call (`fetch(url, { cache: 'force-cache' })` or `next: { revalidate: N }`) or via route config. Don't assume memoized fetches. - **Cache Components** (`cacheComponents: true` in `next.config`, formerly the experimental `dynamicIO`) turns on the `"use cache"` directive and makes **PPR the default**: a static shell plus dynamic holes streamed through `<Suspense>`. Any uncached dynamic data *outside* a Suspense boundary is a build error. - **`"use cache"`** caches a function/component's output; defaults are ~5 min client stale / ~15 min server revalidate; tune with `cacheLife()` and tag with `cacheTag()`. Cache keys include the build ID, the function ID, and serialized arguments/closed-over values — **so a user-specific value in the closure becomes part of the key** (correct) but caching a *component that renders per-user data without keying on the user* leaks across users (MEDIUM–HIGH). Variants: `use cache: private` for per-user. - **Invalidation, and the allowed context differs per API:** `revalidateTag` and `revalidatePath` work from a Server Action **or** a Route Handler. **`updateTag` is Server-Actions-only** — calling it from a Route Handler *throws* (`updateTag can only be called from within a Server Action`), so reach for `revalidateTag` there. This is an invocation-context rule, not a caching-style preference ([Next.js: updateTag](https://nextjs.org/docs/app/api-reference/functions/updateTag), verified 2026-09-16). **ISR** (route `revalidate`) still works. - **Security rule:** never cache a personalized page at a shared cache. If a route reads the session/cookies, it must be dynamic or explicitly `private`. Cache poisoning has been a repeated Next CVE class (below) — CDNs also drop `Vary`, so don't rely on it (`rules/06`). ## 5. Next.js CVE reference (verify ranges at use time) Unpatched, several of these are CRITICAL on their own. Fingerprint the exact version from the lockfile and compare. | CVE / advisory | Class | Fixed in | Note | |---|---|---|---| | **CVE-2025-55182** ("React2Shell") | **RSC deserialization RCE, CVSS 10.0** | react-server-dom-* 19.0.1 / 19.1.2 / 19.2.1 | The React-level flaw; exploited in the wild within hours of 2025-12-03 disclosure | | **CVE-2025-66478** (GHSA-9qr9-h5gf-34mp) | Next.js surface of React2Shell | 15.0.5/15.1.9/15.2.6/15.3.6/15.4.8/15.5.7/16.0.7 | Next 15.x/16.x/14.3-canary.77+ affected; **rotate secrets if it ran unpatched** | | CVE-2025-55184 / -55183 / -67779 / CVE-2026-23864 | RSC DoS + Server-Function source exposure (React2Shell follow-ups) | see 2025-12-11 + later advisories | Upgrade to the latest patch on your line | | **CVE-2025-29927** (GHSA-f82v-jwr5-mffw) | **Middleware auth bypass** via `x-middleware-subrequest`, CVSS 9.1 | 12.3.5/13.5.9/14.2.25/15.2.3 | Self-hosted; strip the header at the proxy; don't rely on middleware for authz | | CVE-2024-46982 (GHSA-gp8f-8m3g-qvj9) | Cache poisoning (Pages Router) | 13.5.7 / 14.2.10 | + CVE-2025-32421 low-sev bypass (<15.1.6) | | CVE-2025-49005 (GHSA-r2fc-ccr8-96c4) | RSC cache poisoning via missing `Vary` | 15.3.3 | App Router | | CVE-2024-34351 (GHSA-fr5h-rqp8-mj6g) | SSRF in Server Actions via Host header | 14.1.1 | Self-hosted redirect handling | | CVE-2025-57822 (GHSA-4342-x723-ch2f) | Middleware redirect → SSRF | 14.2.32 / 15.4.7 | Unsanitized headers into `NextResponse.next()` | | CVE-2024-56332 (GHSA-7m27-7ghc-44w9) | Server Actions DoS | 13.5.8/14.2.21/15.1.2 | | | CVE-2026-44581 (GHSA-ffhc-5mcf-pf4q) | XSS in CSP-nonce apps | 15.5.16 / 16.2.5 | Malformed nonce reflected; cache-poisonable | | CVE-2025-55173 (GHSA-xv57-4mr9-wg8v) / CVE-2025-57752 | next/image content injection / cross-user image cache confusion | 14.2.31 / 15.4.5 | Precondition: permissive `remotePatterns`/`domains` | | GHSA-c4j6-fc7j-m34r + 2026-05 batch | WebSocket-upgrade SSRF; proxy/segment-prefetch bypasses; Cache-Components DoS | latest 15.5.x / 16.2.x | No confirmed CVE on the WS-SSRF advisory; upgrade to current patch | **next/image SSRF:** `remotePatterns` with a `**` wildcard host turns the `/_next/image` optimizer into a blind-SSRF proxy (reachable internal URLs / metadata endpoint), and it follows redirects from an allowed host without re-validating — an open redirect on an allowlisted domain becomes SSRF. Allowlist explicit hosts, protocols, and paths (`rules/07`). ## Audit checklist ```bash # Exact version — compare to the CVE table node -e "console.log(require('./node_modules/next/package.json').version)" grep -E '"(react|react-dom|react-server-dom-webpack|next)"' package.json # Server Actions / Route Handlers — each must authn+authz+validate grep -rn "'use server'" --include='*.ts' --include='*.tsx' app lib grep -rlnE 'export async function (GET|POST|PUT|DELETE|PATCH)' app # -E: without it the # parens are LITERAL in BRE # and this silently finds 0 # Authz only in middleware/layout? (finding) ls middleware.* proxy.* 2>/dev/null; grep -rn 'getServerSession\|auth()\|requireUser' app | head # Server->client data exposure: whole objects as props, env on client # NO negative lookahead: POSIX ERE has none, so `(?!...)` is a syntax error or a literal. # List the env reads, then exclude the public prefix with a second pass. grep -rnE 'process\.env\.[A-Z0-9_]+' --include='*.tsx' app components | grep -v 'NEXT_PUBLIC_' # NB this is a CANDIDATE list, not an absence proof: it greps text, not the Client Component # module graph. A server module imported by a client one is invisible to it. grep -rn "import 'server-only'\|import \"server-only\"" app lib # want: present in data layer # next/image SSRF precondition grep -rn "remotePatterns\|images:\s*{" next.config.* | grep -n '\*\*\|domains' # Caching of personalized routes grep -rn "use cache\|cacheComponents\|force-cache\|revalidate\|Cache-Control" app next.config.* ``` - [ ] Exact Next + react-server-dom versions patched against CVE-2025-55182/-66478 and CVE-2025-29927? - [ ] Every Server Action and Route Handler authenticates, authorizes (ownership/IDOR), and schema-validates input — not relying on middleware? - [ ] No secrets or whole DB rows crossing server→client; data layer marked `server-only`; DTOs minimal? - [ ] Authorization enforced at the data layer, not only in `proxy.ts`/middleware or a layout? - [ ] Caching understood per route; no personalized page cached at a shared cache; `use cache` keyed per-user where needed? - [ ] `next/image` `remotePatterns` limited to explicit trusted hosts (no `**`)? - [ ] `NEXT_SERVER_ACTIONS_ENCRYPTION_KEY` set for multi-replica deploys; `allowedOrigins` configured behind a proxy? -
04-vue.md 6.7 KB
# 04 — Vue 3: Composition API, reactivity, XSS Baseline: Vue 3.5.x. Nuxt-specific concerns (data fetching, `useState`, server routes) are in `rules/05`; hydration in `rules/06`. TypeScript setup depth is in `sota-javascript-typescript`. ## 1. Composition API + `<script setup>` is the default - **For applications, use the Composition API with `<script setup>`** — Vue's own recommendation. Options API remains fully supported and is fine for progressive enhancement / no-build-step sprinkles, but new app code is Composition API. - Don't mix paradigms within a component without reason; consistency aids the compiler and the reader. - **Vue 3.6 (beta, not stable) adds Vapor mode** — an opt-in, per-SFC compilation (`<script setup vapor>`) that drops the virtual DOM for lower overhead. Do not depend on it in production until 3.6 ships stable; verify status before adopting. ## 2. Reactivity pitfalls (where real bugs live) - **`ref` vs `reactive`:** prefer `ref` for primitives and as the default; `reactive` only for objects you won't reassign. **Destructuring a `reactive` object loses reactivity** — the extracted variable is a plain snapshot. Use `toRefs`/`toRef`, or just use `ref`. - **Props destructuring:** since Vue 3.5 the compiler rewrites destructured props to keep them reactive (`const { count } = defineProps(...)`), including as the way to declare defaults with TypeScript. **Caveat:** passing a destructured prop *into* `watch()` or a composable passes a value, not a reactive source — wrap it in a getter `() => count` (or `toValue()` inside the composable). Below 3.5, destructuring loses reactivity entirely — use `props.count`. - **`watch` vs `watchEffect`:** `watch` for explicit sources and precise control (old vs new value, lazy by default); `watchEffect` auto-tracks and runs immediately. Watching a property of a reactive object needs a getter: `watch(() => obj.id, …)`. Deep watches are expensive — Vue 3.5 supports a numeric `deep` depth limit; use `once: true` (3.4+) for one-shot. - **Watcher/effect lifecycle:** watchers/computed created *synchronously* in `setup` auto-dispose on unmount. Ones created **asynchronously** (after an `await`, in a callback, in a timer) do **not** — they leak. Wrap detached reactive work in `effectScope()` and call `scope.stop()`, or use `onScopeDispose()` in composables. - **`shallowRef`/`shallowReactive` for large structures** (big lists, external instances, non-reactive class objects): reactivity only at the root, so replace the root to trigger updates and treat nested data as immutable. `triggerRef` forces a refresh. This is the main Vue perf lever alongside `v-once`/`v-memo` and list virtualization (`sota-performance`). ## 3. Component API design - **`defineModel()`** (Vue 3.4+) is the modern two-way binding — compiles to the `modelValue` prop + `update:modelValue` event, supports multiple/named models and modifiers. Prefer it over manually declaring the prop/emit pair. - **Type-based `defineProps`/`defineEmits`** is the recommended TypeScript style (`defineProps<{ id: string; count?: number }>()`); you cannot mix the type-based and runtime-object forms in one call. Emits use the tuple/type syntax (3.3+). - **Provide/inject** for cross-cutting dependencies; type the injection key (`InjectionKey<T>`). For SSR, remember provide/inject is per-app-instance — which is exactly how you avoid cross-request leakage (`rules/06`). - **Composables** (`useX`) are Vue's unit of logic reuse — return refs/computed, accept getters or `MaybeRefOrGetter` + `toValue()` for flexible inputs, and clean up with `onScopeDispose`. ## 4. Vue-specific XSS and injection Vue auto-escapes text interpolations and attribute bindings (including in SSR — the core `escapeHtml` escapes `" ' & < >`). The sinks are where you leave that protection. (General theory: `sota-code-security` rules/05.) - **`v-html` bypasses escaping** — it sets `innerHTML`. User HTML through `v-html` is never safe unless sanitized (allowlist sanitizer) or shown only to its own author in a sandboxed context. ```vue <!-- BAD — HIGH: XSS --> <div v-html="comment.body" /> <!-- GOOD — sanitize first, or render as text --> <div v-html="sanitize(comment.body)" /> <!-- DOMPurify / server allowlist sanitizer --> <div>{{ comment.body }}</div> <!-- best: no HTML at all --> ``` - **Never use untrusted content as a component `template`** — Vue's docs call this rule #1: a dynamic template is arbitrary JS execution. Don't build `template:` (or a runtime-compiled component) from user input. - **Never mount Vue on DOM that contains server-rendered user content.** HTML that is safe *as HTML* can be unsafe *as a Vue template* (mustache/`v-` directives in the markup get compiled). Mount only on markup you control (hydration-XSS in `rules/06`). - **URL injection:** `:href="userUrl"` / `:src` allow `javascript:` and `data:` — validate the scheme before binding (sanitize on the backend before storage, per Vue's guidance). **Style injection:** binding user data to `:style` enables clickjacking-style overlays — bind specific, typed properties, not a raw string. Never bind user content to event handlers (`@click="userValue"`). ## Audit checklist ```bash # HTML-injection sink — each hit needs a sanitizer on the source grep -rn 'v-html' --include='*.vue' src components pages # Dynamic templates / runtime compilation from input (arbitrary JS) grep -rnE 'template:\s*[^\x27"]*(\$|props|user|input)' --include='*.vue' --include='*.ts' src grep -rn 'compile(' --include='*.ts' src # URL / style / handler injection grep -rnE ':href=|:src=|:style=' --include='*.vue' src | grep -iv 'sanitiz\|allow' # Reactivity leak/loss patterns grep -rnE 'const \{[^}]+\}\s*=\s*reactive\(' --include='*.vue' --include='*.ts' src # destructuring reactive => lost reactivity grep -rnE 'watch(Effect)?\(' --include='*.vue' src | head # verify async-created ones are scoped grep -rn 'ref(' --include='*.ts' src/composables 2>/dev/null # module-level refs? (rules/06 SSR leak) ``` - [ ] Every `v-html` fed sanitizer output, not raw user/CMS HTML (or rendered as text instead)? - [ ] No component `template`/runtime-compiled component built from user input? - [ ] Vue never mounted on DOM containing server-rendered user content? - [ ] `:href`/`:src` scheme-validated (no `javascript:`/`data:`); `:style` bound as typed props, not raw strings? - [ ] No reactivity lost by destructuring `reactive` (use `toRefs`/`ref`); destructured props not passed bare into `watch`/composables? - [ ] Async-created watchers/effects scoped (`effectScope`/`onScopeDispose`) so they don't leak? - [ ] `defineModel` for two-way binding; type-based `defineProps`; `shallowRef` for large structures? -
05-nuxt.md 8.7 KB
# 05 — Nuxt 4: data fetching, state, server routes, CVEs Baseline: Nuxt 4.4.x on Nitro 2.x / h3 1.x. Vue fundamentals are in `rules/04`; hydration and SSR state in `rules/06`; the cross-framework security boundary in `rules/07`. **Re-verify CVE ranges at use time.** ## 1. Data fetching — pick the right primitive Getting this wrong causes double-fetches, hydration mismatches, and waterfalls. - **`$fetch` alone in `setup` fetches twice** — once on the server, once again during client hydration — because its result isn't transferred in the payload. Use it only for **client-only / event-driven** calls (a button handler, a `POST`). - **`useFetch`** is the SSR-safe wrapper: fetches once on the server and transfers the result to the client via the payload. The default in components. Keys default to a hash of the URL/options; calls sharing a key share the `data`/`error`/`status` refs. - **`useAsyncData`** wraps arbitrary async logic (a CMS SDK, multiple calls, custom transform) with the same once-and-transfer semantics; give it an explicit key. - `lazy: true` (or `useLazyFetch`) doesn't block navigation; `server: false` makes it client-only. Use `pick`/`transform` to **shrink the payload** — whatever these return is serialized into the HTML (`rules/06`, `rules/07`). ## 2. SSR-safe shared state — never a module-level ref - **`useState(key, init)`** is the SSR-friendly `ref`: its value is serialized into the payload and restored on the client, and it's shared by key across components. - **A module-level `const state = ref()` (outside `setup`) is a cross-request leak** on the server — the Nitro process reuses it across all users' requests (one user's data served to another) and it grows unbounded (memory leak). Nuxt's docs call this out explicitly. This is the single most important Nuxt SSR footgun (`rules/06`). - `useState` values must be **serializable** (no classes/functions/symbols). For richer state management, **Pinia** (v3) is the recommended store and is SSR-safe by design (a fresh store per request). ## 3. `runtimeConfig` — the server/client secret boundary ```ts export default defineNuxtConfig({ runtimeConfig: { apiSecret: '', // SERVER-ONLY — never sent to the client public: { apiBase: '/api' }, // PUBLIC — serialized into the payload, visible to all }, }) ``` - **Only `runtimeConfig.public.*` reaches the client** (it's in the payload). Everything at the root is server-only. **A secret under `public` is a client-side secret leak (CRITICAL)** — same failure class as `NEXT_PUBLIC_`/`VITE_` (`rules/07`). - **Runtime override via env:** a `NUXT_`-prefixed env var overrides the matching key (`NUXT_API_SECRET`, `NUXT_PUBLIC_API_BASE`) — but the key must already exist in `nuxt.config` to be overridable. `.env` is read at dev/build time but **not** by the built production server; provide real env vars in production. ## 4. Nitro server routes — public API endpoints Files under `server/api/` and `server/routes/` are Nitro handlers — **public HTTP endpoints**, same trust model as any API (`sota-api-design`, `sota-code-security`). ```ts // server/api/posts/[id].delete.ts export default defineEventHandler(async (event) => { const { user } = await requireUserSession(event) // 1. authenticate const { id } = await getValidatedRouterParams(event, z.object({ id: z.string().uuid() }).parse) // 3. validate const post = await db.post.find(id) if (post.authorId !== user.id) throw createError({ statusCode: 403 }) // 2. authorize await db.post.delete(id) }) ``` - **Validate every input** with `getValidatedQuery` / `readValidatedBody` / `getValidatedRouterParams` + a schema (Zod). Unvalidated query/body/params are the usual injection and IDOR entry points. Use `createError({ statusCode })` for typed responses; an uncaught throw is a 500. - **Authenticate + authorize in the handler.** `nuxt-auth-utils` (sealed, encrypted session cookies; `requireUserSession`/`setUserSession`; scrypt hashing; OAuth + passkeys) is a solid, maintained baseline — it needs a real server (`nuxt build`, not `nuxt generate`). Check ownership on every resource to prevent IDOR. - **Server-route responses are `JSON.stringify`'d** (unlike the devalue-serialized page payload) — return primitives/plain objects and filter them (no raw rows). ## 5. Hybrid rendering (`routeRules`) and islands - **`routeRules`** in `nuxt.config` set the render mode per route pattern: `ssr: false` (client-only), `prerender: true` (SSG), `swr: <ttl>` (server/proxy cache + stale-while-revalidate), `isr: <ttl>` (like swr but pushed to CDN on supporting platforms; `isr: true` persists until next deploy). Also `redirect`, `headers`, `cors`, `noScripts`. **Security:** an `swr`/`isr`/cached route must not serve personalized content — the cache is shared (`rules/06`). A `routeRules` matcher that doesn't match the actual (case-sensitive) route can bypass an intended rule — the class behind CVE-2026-53721 below. - **Server components / islands are experimental** in Nuxt 4 (enable component islands; `.server.vue` rendered via `<NuxtIsland>`): single root element, props travel as URL query params (keep them small), no route middleware inside an island. Given the island advisories below, treat as experimental and don't put authz decisions inside island rendering. ## 6. Nuxt / Nitro / h3 / IPX / devalue CVE reference (verify at use time) | CVE / advisory | Class | Fixed in | |---|---|---| | **CVE-2025-27415** (GHSA-jvhm-gjrh-3h93) | Nuxt CDN **cache poisoning DoS** via a `?…_payload.json`-style query rendering the route as JSON, High 7.5 | Nuxt **3.16.0** | | **CVE-2026-53721** (GHSA-mm7m-92g8-7m47) | `routeRules` **middleware bypass** via case-sensitivity mismatch, High | Nuxt **4.4.7 / 3.21.7** | | CVE-2026-53722 (GHSA-934w-87qh-qr26) | Reflected **XSS in `<NuxtLink>`** via `javascript:`/`data:` URLs | Nuxt 4.4.7 / 3.21.7 | | GHSA-hg3f-28rg-4jxj | Route middleware **not enforced** rendering `.server.vue` via `/__nuxt_island/…` | see advisory (2026-05) | | CVE-2025-54387 (GHSA-mm3p-j368-7jcr) | **IPX path traversal** (prefix-match bypass) — the `@nuxt/image` optimizer | IPX **1.3.2 / 2.1.1 / 3.1.1** | | CVE-2026-33128 (GHSA-22cc-p3c6-wpvm) + follow-ups | **h3 SSE injection** via unsanitized newlines (High); serveStatic path traversal; middleware bypass | h3 **1.15.6 / 2.0.1-rc.15** | | CVE-2025-57820 (GHSA-vj54-72f3-p5jv) | **devalue prototype pollution** on `parse` (the Nuxt payload deserializer), High | devalue **5.3.2** | | CVE-2026-30226 (GHSA-cfw5-2vxh-hr84) + DoS advisories | devalue prototype pollution / parse DoS | devalue **5.6.4** (+ later) | - **`nuxt-security` module** is the maintained hardening layer: OWASP-pattern security headers, CSP (with nonce support for SSR — verify the CSP docs for your mode), rate limiting, request-size limits, CORS, allowed-methods, XSS input validation, CSRF. Strongly consider it for any Nuxt app exposed to the internet. ## Audit checklist ```bash # Exact versions vs the CVE table node -e "const p=require('./package.json');console.log(p.dependencies?.nuxt||p.devDependencies?.nuxt)" grep -E '"(nuxt|nitropack|h3|ipx|@nuxt/image|devalue|pinia)"' package.json # Secret under public runtimeConfig (CRITICAL) grep -rnA8 'runtimeConfig' nuxt.config.* | grep -iE 'public' -A6 | grep -iE 'secret|key|token|password' # Module-level refs / state outside setup (cross-request leak) grep -rnE '^(export )?const \w+\s*=\s*(ref|reactive)\(' --include='*.ts' composables server utils 2>/dev/null # $fetch in setup (double fetch), server routes without validation grep -rn '\$fetch(' --include='*.vue' pages components | grep -v useFetch grep -rLn 'getValidated\|readValidatedBody\|requireUserSession\|\.parse(' server/api server/routes 2>/dev/null # routeRules that cache — must not be personalized grep -rnE 'swr|isr|prerender|ssr:\s*false' nuxt.config.* # NuxtLink / URL sinks grep -rn ':to=\|:href=' --include='*.vue' pages components | grep -iv 'sanitiz' ``` - [ ] Nuxt/Nitro/h3/IPX/devalue versions patched against the table (esp. CVE-2025-27415, CVE-2026-53721, devalue pollution)? - [ ] No secret under `runtimeConfig.public`; server secrets at the root only? - [ ] No module-level `ref`/`reactive` state outside `setup` (cross-request leak); `useState`/Pinia used instead? - [ ] Every `server/api` handler authenticates, authorizes (ownership), and schema-validates input; responses filtered? - [ ] `useFetch`/`useAsyncData` (not bare `$fetch`) for SSR data; payload shrunk via `pick`/`transform`? - [ ] No `swr`/`isr`/`prerender` route serving personalized content; `routeRules` matchers actually match? - [ ] `nuxt-security` (or equivalent headers/CSP/rate-limit) in place for internet-facing apps? -
06-ssr-hydration.md 9.8 KB
# 06 — SSR & hydration: mismatches, serialization, caching, CSP Cross-cutting concerns of server rendering, shared by Next and Nuxt. Framework specifics are in `rules/03`/`rules/05`; the consolidated security boundary in `rules/07`. ## 1. Hydration mismatches — cause and correct fix Hydration is the client attaching event handlers to server-rendered HTML, assuming the two render trees are identical. When they diverge: - **React** may recover from some mismatches but gives *no guarantee* attribute differences are patched, and if a mismatch forces it, React **discards the server HTML and re-renders the whole root on the client** — losing the SSR benefit and flashing. Treat every mismatch as a bug. - **Vue/Nuxt** logs a mismatch warning and patches the DOM toward the client render; invalid HTML nesting silently corrupts the tree. **Common causes** (per react.dev and vuejs.org): - Non-deterministic values in render: `Date.now()`/`new Date()`, `Math.random()`, locale/timezone-dependent formatting, `crypto.randomUUID()`. - Branching on `typeof window`, `matchMedia`, `localStorage`, `navigator` during render (server and client take different branches). - **Invalid HTML nesting** (`<p><div>`, `<a><a>`, a `<div>` inside `<table>` without `<tbody>`): the browser's parser auto-corrects, so the client tree no longer matches the server string. - Browser extensions mutating the DOM before hydration (can't fully control; don't let it mask real mismatches). **Correct fixes:** - Make render **deterministic**. Move browser-only reads into `useEffect` (React) / `onMounted` (Vue) so they run after hydration, or gate with a mounted flag / `<ClientOnly>` (Nuxt) / dynamic import with `ssr: false`. - **Stable IDs:** React `useId` (with `identifierPrefix` on `hydrateRoot` for multiple roots) and Vue 3.5 `useId()` — never random IDs across the boundary. - For genuinely unavoidable divergence (a live timestamp), scope the suppression as narrowly as possible: React `suppressHydrationWarning` (one level deep, intended for exactly this), Vue 3.5 `data-allow-mismatch`. These silence the warning for *that node only* — never blanket them, and never use them to paper over a real bug. - **Security:** never "resolve" a mismatch by injecting server-side user-controlled HTML into the tree. Browser parser normalization of malformed markup is the same parser-differential class that drives mutation-XSS — escape/sanitize instead of hand-patching the DOM to match. (Sanitizer guidance: `rules/02`/`rules/04`.) ## 2. SSR state serialization — an XSS sink To avoid double-fetching, SSR frameworks embed fetched state in the HTML as an inline `<script>`. Done naively this is a script-injection hole. - **Naked `JSON.stringify` into a `<script>` is XSS.** JSON does not escape `<`, so a value containing `</script>` closes the tag early and injects markup; `<!--` and `<script` can also break parsing, and `U+2028`/`U+2029` break older JS string parsers. The fix is to escape `<`, `>`, `&`, and the line separators to `\uXXXX` before embedding (or set the data via `<script type="application/json">` + `textContent`, parsed with `JSON.parse`). ```js // BAD — HIGH: state contains user data → </script> breakout html += `<script>window.__DATA__=${JSON.stringify(state)}</script>`; // GOOD — escape the HTML-significant characters first const safe = JSON.stringify(state).replace(/</g,'\\u003c').replace(/>/g,'\\u003e') .replace(/&/g,'\\u0026').replace(/\u2028/g,'\\u2028').replace(/\u2029/g,'\\u2029'); ``` - **Prefer the framework serializer** — you rarely hand-roll this: - **Nuxt** serializes the payload with **devalue** (handles `Date`/`Map`/`Set`/refs and escapes `</script>` + line separators). But devalue's *parse* side has had prototype-pollution CVEs (CVE-2025-57820, CVE-2026-30226) and DoS advisories — keep it patched (`rules/05`). - **Next** embeds the RSC/flight payload via `self.__next_f.push([...])`. - **`serialize-javascript`** (used to embed functions/regex): CVE-2020-7660 (RCE, fixed 3.1.0) and CVE-2024-11831 (XSS via unescaped URL objects, fixed **6.0.2**) — if it's in the tree, verify the version. - **Only serializable data belongs in the payload**, and *everything* in it is public — never let a secret, token, or full DB row reach `useState`/a client prop (`rules/07`). ## 3. Cross-request state pollution On the server the module graph is loaded **once per process** and reused for every request. Module-level mutable state is therefore shared across all users. - **The bug:** `let currentUser` / `const cart = reactive([])` / a singleton client holding per-request data at module scope. Under load, one user sees another's data, and memory grows unbounded. This is a confidentiality breach, not just a leak. - **React/Next:** don't hold request data in module globals; use request-scoped APIs (`cookies()`/`headers()`, React `cache()` for per-request memoization, the DAL). - **Vue/Nuxt:** create fresh app/router/store instances per request (Nuxt does this for you); use `useState`/Pinia, never a module-level `ref` (`rules/05`). Share request-scoped values via app-level `provide`/`inject`, not module scope. - Audit any module-level `let`/mutable singleton in server-reachable code. ## 4. Caching personalized SSR safely Caching is where SSR bugs become cross-user data leaks. - **Personalized (auth/cookie-dependent) responses must be `Cache-Control: private`** (browser only) or uncached — never `public`/`s-maxage` at a shared CDN. - **`Vary` is not a reliable isolation mechanism at CDNs.** Cloudflare ignores `Vary` values; CloudFront strips `Vary` before returning. If correctness depends on the cache keying by a header, verify your CDN actually honors it — prefer explicit per-user cache keys or no shared caching. - **Web cache deception / poisoning** (PortSwigger "Gotta cache 'em all", 2024) exploit URL-parsing differences between CDN and origin so a crafted path is cached as a static asset while the origin served a private page. Both Next (CVE-2024-46982, CVE-2025-49005, CVE-2025-32421) and Nuxt (CVE-2025-27415) have shipped cache-poisoning CVEs — keep patched and don't hand a CDN an ambiguous cache key. Next's RSC payload uses a `Rsc:`/`_rsc=` scheme that has been a poisoning vector; a request missing the buster but carrying the header can poison HTML with an RSC payload. - **ISR/SWR** trade freshness for speed — only for non-personalized content, and think through the stale window (`rules/01`, `rules/05`). ## 5. CSP with streaming SSR A strict, nonce/hash-based CSP is the highest-leverage defense-in-depth for these apps — but it interacts with rendering. - **Next (official guide):** generate a per-request nonce in `proxy.ts`, set it on the CSP header and an `x-nonce` request header; Next applies it to its own scripts. Use `script-src 'self' 'nonce-<n>' 'strict-dynamic'`. **Nonces force dynamic rendering** — they're incompatible with static generation, ISR, and PPR (there's no request at build time). Opt a page into dynamic with `await connection()`. The static-friendly alternative is experimental hash-based CSP via SRI (App Router). Dev needs `'unsafe-eval'` (not prod). - **Nuxt:** the `nuxt-security` module supplies CSP — runtime nonces for SSR (Nitro sets the header), build-time SHA hashes for SSG (`<meta>`, no server) — both with `'strict-dynamic'` and no `'unsafe-inline'`. - Without nonces/hashes, framework output typically requires `'unsafe-inline'`, which neuters CSP against injected inline scripts. If you're going to have a CSP, wire the nonce/hash — a CSP with `'unsafe-inline'` on `script-src` is close to no CSP. - CSP is defense-in-depth over escaping/sanitization (`rules/02`/`rules/04`), never a replacement. General CSP/headers depth: `sota-code-security` rules/05, `sota-network-security`. ## Audit checklist ```bash # Non-determinism in render (hydration bugs) grep -rnE '(Date\.now|new Date|Math\.random|crypto\.randomUUID)\(' --include='*.tsx' --include='*.vue' src app components pages | grep -v useEffect grep -rnE 'typeof window|localStorage|matchMedia|navigator\.' --include='*.tsx' --include='*.vue' src app pages # Blanket mismatch suppression (smell if widespread) grep -rn 'suppressHydrationWarning\|data-allow-mismatch' --include='*.tsx' --include='*.vue' src app pages # Hand-rolled state serialization into <script> grep -rnE 'JSON\.stringify' --include='*.ts' --include='*.tsx' server app | grep -i 'script\|__DATA__\|innerHTML' grep -E '"serialize-javascript"' package.json # verify >=6.0.2 # Cross-request state pollution: module-level mutable state in server code grep -rnE '^(export )?(let|const) \w+\s*=\s*(reactive|ref|new |\[\]|\{\})' --include='*.ts' server lib composables utils 2>/dev/null # Caching / CSP grep -rn 'Cache-Control\|s-maxage\|Vary' --include='*.ts' server app middleware.* proxy.* grep -rn "Content-Security-Policy\|nonce\|strict-dynamic\|nuxt-security" --include='*.ts' app proxy.* middleware.* nuxt.config.* ``` - [ ] Render deterministic — no `Date`/random/`window`/locale branching outside effects/`onMounted`; stable `useId`? - [ ] Mismatch suppression scoped to individual unavoidable nodes, never blanket, never patched with injected user HTML? - [ ] SSR state serialized via the framework serializer or `<`-escaped JSON (no naked `JSON.stringify` into `<script>`); `serialize-javascript` ≥ 6.0.2; devalue patched? - [ ] No secret/token/full row in the serialized payload (`useState`/client props)? - [ ] No module-level mutable state in server-reachable code (cross-request leak)? - [ ] Personalized responses `Cache-Control: private`/uncached; not relying on `Vary` at the CDN; framework patched against cache-poisoning CVEs? - [ ] CSP present and nonce/hash-based (not `'unsafe-inline'` on `script-src`), with the dynamic-rendering trade-off understood? -
07-security.md 8.6 KB
# 07 — Framework security: boundary, authz, SSRF, CVEs The security pass for both stacks. This consolidates the framework-specific angles; generic web-appsec theory (injection classes, session/JWT mechanics, crypto) lives in `sota-code-security`, secrets handling in `sota-secrets-management`, supply chain in `sota-devsecops`. **Re-verify every CVE at use time.** ## 1. The server/client secret boundary The one boundary unique to these frameworks, and the most common leak. - **Build-time-inlined public env is public forever:** `NEXT_PUBLIC_*`, Nuxt `runtimeConfig.public.*`, and Vite `VITE_*` are substituted into the client bundle at build time. **A secret in any of them is a client-side secret leak (CRITICAL)** — and rotating it means a rebuild, not just a config change. Server secrets go in unprefixed env / root `runtimeConfig`, read only in server code. - **Everything crossing server→client is public:** RSC props (Next), the serialized payload (`useState`/`useFetch` data in Nuxt), and anything in an inline `<script>`. Pass **minimal DTOs**, never raw DB rows, tokens, internal flags, or `process.env`. - **Enforce the boundary mechanically:** `import 'server-only'` (Next/bundler) makes a server module a build error if imported by client code; keep data access and secrets behind it. In Nuxt, keep secrets in `server/` and root `runtimeConfig`. - **Production source maps** can re-expose "server" logic and comments if uploaded publicly — ship them to your error tracker privately, not to the CDN. ## 2. Authorization placement (post-CVE-2025-29927) The middleware-bypass CVE (CVE-2025-29927) made the lesson concrete: **a single authz checkpoint at the edge is not enough.** - **Middleware / `proxy.ts` (Next) and route middleware (Nuxt) are optimizations**, not the security boundary. They can be bypassed (a spoofed header; a matcher that stops covering a route; the Nuxt case-sensitivity bypass CVE-2026-53721) and don't re-run where you assume. - **Authorize next to the data.** Every Server Action, Route Handler, RSC data read, and Nitro handler independently: (1) authenticates, (2) authorizes the *specific resource* (ownership/role), (3) validates input. A Data Access Layer (Next) or a shared `requireUserSession` + policy helper (Nuxt) is how you avoid forgetting. - **IDOR is the dominant framework authz bug:** an action/route that takes an `id` and acts on it without checking the caller owns it. Test for it (`sota-testing` authz tests). Never trust a client-supplied role/flag (`?isAdmin=true`, a hidden field). - **Layouts don't re-authorize on client navigation** (Next) — don't put the only check there. ## 3. SSRF surfaces specific to SSR apps Server-side rendering means the server makes outbound requests — attacker-influenced URLs become SSRF. - **Image optimizers:** `next/image` `remotePatterns`/`domains` with a `**` wildcard, and IPX/`@nuxt/image` (path-traversal CVE-2025-54387), turn the optimizer into a proxy to internal URLs and the cloud metadata endpoint (169.254.169.254). Allowlist explicit hosts, protocols, ports, and path prefixes — and note the optimizer follows redirects from an allowed host *without re-validating*, so an open redirect on an allowlisted domain becomes SSRF. - **User-URL fetchers:** og-image/link-preview/"import from URL" features fetch a user-supplied URL server-side. Validate the host against an allowlist, block private ranges and the metadata IP, disable/limit redirects, and set timeouts (`sota-code-security` rules/09, `sota-network-security` for egress control). - **Header trust:** building absolute URLs or redirects from `Host`/`X-Forwarded-Host` (Next SSRF CVE-2024-34351, CVE-2025-57822; Nuxt navigate advisories) lets an attacker redirect server fetches. Pin a canonical base URL from config, don't reflect the Host header. Never pass unsanitized inbound headers into `NextResponse.next()`/redirects. - **Open redirects:** validate `redirect`/`next`/`returnTo` targets against a same-origin/allowlist check before redirecting. ## 4. Consolidated framework CVE reference (verify ranges at use time) Fingerprint exact versions from the lockfile; unpatched criticals are findings on their own. Full detail in `rules/03` (Next/React) and `rules/05` (Nuxt/Nitro/h3). **React / Next.js** - **CVE-2025-55182 "React2Shell"** — RSC deserialization **RCE, CVSS 10.0**; react-server-dom-* fixed 19.0.1 / 19.1.2 / 19.2.1. Next surface **CVE-2025-66478** fixed on every 15.x/16.x line (e.g. 15.5.7 / 16.0.7); **rotate secrets if it ran unpatched.** Follow-up DoS/exposure: CVE-2025-55184/-55183/-67779, CVE-2026-23864. - **CVE-2025-29927** — middleware auth bypass, CVSS 9.1; fixed 12.3.5/13.5.9/14.2.25/15.2.3. - Cache poisoning: CVE-2024-46982 (13.5.7/14.2.10), CVE-2025-49005 (15.3.3), CVE-2025-32421. SSRF: CVE-2024-34351 (14.1.1), CVE-2025-57822 (14.2.32/15.4.7), GHSA-c4j6-fc7j-m34r (WebSocket). CSP-nonce XSS: CVE-2026-44581 (15.5.16/16.2.5). next/image: CVE-2025-55173 / CVE-2025-57752 (14.2.31/15.4.5). DoS: CVE-2024-56332. **Vue / Nuxt / Nitro / h3 / IPX / devalue** - Nuxt cache-poisoning DoS **CVE-2025-27415** (3.16.0); `routeRules` bypass **CVE-2026-53721** (4.4.7/3.21.7); `<NuxtLink>` XSS CVE-2026-53722; island authz advisories (GHSA-hg3f-28rg-4jxj). - IPX path traversal **CVE-2025-54387** (1.3.2/2.1.1/3.1.1). - h3 SSE injection **CVE-2026-33128** + serveStatic traversal / middleware bypass (1.15.6 / 2.0.1-rc.15). - devalue prototype pollution **CVE-2025-57820** (5.3.2), **CVE-2026-30226** (5.6.4) + DoS advisories. serialize-javascript CVE-2024-11831 (6.0.2). The pattern across all of these: **the fix is almost always "upgrade."** Automated dependency updates + a fast patch path is the actual control (`sota-devsecops`). ## 5. Framework security hygiene - **CSP** nonce/hash-based, not `'unsafe-inline'` (`rules/06`); `nuxt-security` or a Next `proxy.ts` header layer. Plus the standard headers (HSTS, `nosniff`, `frame-ancestors`) — `sota-code-security` rules/05. - **Supply chain:** these apps have deep dependency trees (a framework pulls hundreds of transitive packages). Lockfile committed, `npm audit`/`osv-scanner` in CI, provenance where available (`sota-devsecops`). Client-shipped dependencies are also an XSS surface (a compromised npm package runs in your users' browsers). - **Error handling:** don't leak stack traces / internal paths to the client in production; don't render user-controlled error text as HTML (`rules/02`). - **Rate limiting** on Server Actions / Route Handlers / Nitro routes — they're public endpoints (`sota-api-design` rules/07). ## Audit checklist ```bash # Public-env secret leak (CRITICAL) grep -rnE '(NEXT_PUBLIC_|VITE_)[A-Z_]*(SECRET|KEY|TOKEN|PASSWORD|PRIVATE)' --include='*.ts' --include='*.tsx' --include='*.vue' . grep -rnA8 'runtimeConfig' nuxt.config.* | grep -iE 'public' -A6 | grep -iE 'secret|key|token' # Server->client exposure & the server-only guard grep -rn "import 'server-only'\|server/" app lib server | head grep -rnE 'process\.env\.' --include='*.tsx' --include='*.vue' app components pages | grep -iv 'NEXT_PUBLIC\|NODE_ENV' # Authz only at the edge? enumerate actions/handlers and check each grep -rn "'use server'" app lib; ls -1 app/**/route.ts server/api/**/*.ts 2>/dev/null # SSRF surfaces grep -rn 'remotePatterns\|images:\s*{\|ipx\|@nuxt/image' next.config.* nuxt.config.* | grep -n '\*\*\|domains' grep -rnE '\$?fetch\(|ofetch\(|axios\.|got\(' --include='*.ts' server app | grep -iE 'req\.|query|params|headers|host' grep -rnE 'X-Forwarded-Host|req\.headers\.host|getRequestHost' --include='*.ts' server app proxy.* middleware.* # CVE fingerprint grep -E '"(react|react-dom|react-server-dom-webpack|next|nuxt|nitropack|h3|ipx|devalue|serialize-javascript)"' package.json ``` - [ ] No secret in `NEXT_PUBLIC_`/`VITE_`/`runtimeConfig.public`; server secrets behind `server-only`/`server/`? - [ ] No raw rows/tokens/`process.env` crossing server→client; minimal DTOs only? - [ ] Every Server Action / Route Handler / Nitro route authenticates, authorizes the specific resource (no IDOR), and validates input — not relying on edge middleware? - [ ] `next/image`/IPX `remotePatterns` allowlisted to explicit hosts (no `**`); user-URL fetchers block private ranges + metadata IP + redirects? - [ ] Absolute URLs/redirects built from config, not reflected `Host`/`X-Forwarded-Host`; redirect targets allowlisted? - [ ] All framework + loader deps (react-server-dom, next, nuxt, h3, ipx, devalue, serialize-javascript) patched against §4; automated updates on? - [ ] Nonce/hash CSP + standard security headers; rate limiting on public endpoints; production errors not leaked as HTML?
-
-
SKILL.md 10.6 KB
--- name: sota-web-frameworks description: >- State-of-the-art engineering rules (2026) for the JavaScript SSR meta-frameworks: React 19 + Next.js (App Router, React Server Components, Server Actions) and Vue 3 + Nuxt 4 (Nitro server routes, composables) — plus the cross-cutting concerns of server rendering: hydration correctness, SSR state serialization, the server/client trust boundary, and framework-specific security and CVEs. Use when building or auditing any React/Next or Vue/Nuxt app — components, RSC/client boundaries, Server Actions or Nitro routes, data fetching and caching, hydration mismatches, SSR/SSG/ISR strategy, or framework CVE exposure. Complements sota-javascript-typescript, sota-frontend-design, sota-code-security, and sota-performance. Trigger keywords: React, Next.js, App Router, React Server Components, RSC, Server Actions, use client, use server, Vue, Nuxt, Nitro, Pinia, composable, script setup, SSR, hydration, hydration mismatch, use cache, PPR, ISR, proxy.ts, middleware, useFetch, useState, runtimeConfig, CSP nonce, devalue. --- # SOTA Web Frameworks (2026) ## Purpose This skill encodes the 2026 state of the art for the two dominant JavaScript SSR stacks — **React + Next.js** and **Vue + Nuxt** — and the server-rendering concerns they share. It is deliberately framework-specific: the traps that matter here (the RSC server/client boundary, Server Actions as public endpoints, hydration mismatches, SSR state leaking across requests, `runtimeConfig`/`NEXT_PUBLIC_` secret boundaries) do not exist at the plain-language level. Two modes: - **BUILD** — writing or modifying components, routes, and data-fetching to this standard. - **AUDIT** — reviewing an existing app and reporting findings. Read SKILL.md fully; load `rules/*.md` on demand per the index below. **Verify every version/CVE claim at use time** — this file's facts were primary-sourced 2026-07 but framework security moves weekly (see the 2025-12 React Server Components RCE). ## Scope boundaries (what lives elsewhere) This skill stacks *on top of* the general skills — load them together, don't duplicate: - **`sota-javascript-typescript`** — strict `tsconfig`, type design, promises, Node hardening, npm supply chain. The *language*; this skill is the *framework*. - **`sota-frontend-design`** — visual design, layout, components-as-UX, WCAG 2.2 accessibility, motion. This skill covers component *engineering*, not *design*. - **`sota-code-security`** — generic XSS/CSRF/SSRF/authn/authz/crypto theory. This skill covers the *framework-specific* expression of those (RSC data exposure, Server Action authz, `v-html`, next/image SSRF). - **`sota-performance`** rules/06 — Core Web Vitals, bundle budgets, image/font loading. This skill covers render-strategy choice (SSR/SSG/ISR/PPR) and hydration. - **`sota-api-design`** — REST/GraphQL contract design for the routes themselves. ## BUILD mode 1. **Establish context first.** Read `package.json` for the exact React/Next or Vue/Nuxt majors and the render mode in use (App vs Pages Router; Nuxt SSR vs `ssr: false`). Match the project's baseline — no Server Actions on a Pages-Router app, no `defineModel` below Vue 3.4. Confirm the versions are supported and patched against the CVE tables in `rules/03`/`rules/05`. (`rules/01`) 2. **Default to the current idiom:** React function components + hooks (let the React Compiler memoize — don't hand-write `useMemo` everywhere); Vue Composition API with `<script setup>`. Server Components by default in Next App Router, `"use client"` only at the leaves that need interactivity. (`rules/02`, `rules/04`) 3. **The server/client boundary is a security boundary, not just a perf one.** Every prop crossing server→client is serialized into the HTML/RSC payload and is public. Authorization lives *next to the data* (a Data Access Layer / validated server route), never only in middleware or a layout. (`rules/03`, `rules/07`) 4. **Treat every Server Action and Nitro route as a public, unauthenticated HTTP endpoint** until it validates input and checks authz itself — even if it looks internal or is never imported. (`rules/03`, `rules/05`, `rules/07`) 5. **Get hydration right by construction:** deterministic render (no `Date.now()`, `Math.random()`, or `window` in render), stable IDs via `useId`, SSR-safe shared state (`useState`/per-request instances, never module-level refs). (`rules/06`) 6. **Serialize SSR state safely** (framework serializer or escaped JSON, never naked `JSON.stringify` into `<script>`) and **wire CSP** (nonce or hash) knowing it forces dynamic rendering in Next. (`rules/06`, `rules/07`) 7. **Tests accompany code** (`sota-testing`): component tests plus at least one test that exercises the server/client boundary or a server route's authz. ## AUDIT mode 1. **Fingerprint + patch-check first.** Pin exact framework/loader versions from the lockfile and diff them against the CVE tables in `rules/03` and `rules/05`. An unpatched React2Shell (CVE-2025-55182) or middleware bypass (CVE-2025-29927) is CRITICAL on its own, before any code is read. 2. **Trace the trust boundary.** Grep `"use client"`/`"use server"`, `defineProps`, `runtimeConfig`, `NEXT_PUBLIC_`/`NUXT_PUBLIC_`; find where server data crosses to the client and where authz is enforced. Middleware/layout-only authz is a finding. 3. **Run each relevant rules file's Audit checklist** (grep-driven), then read for design: hydration determinism, SSR state isolation, caching of personalized pages. 4. **Verify every finding** — a `v-html` fed a constant is not XSS; a Server Action that re-checks the session is not IDOR. Note mitigations already present. ### Severity conventions | Severity | Meaning | Examples | |---|---|---| | CRITICAL | Exploitable now / RCE / data loss | Unpatched RSC deserialization RCE; middleware-bypass auth on the only authz layer; secret in `NEXT_PUBLIC_`/`public` runtimeConfig; `unserialize`-class sink | | HIGH | Exploitable with preconditions | Server Action / Nitro route with no authz (IDOR); SSRF via next/image `**` or user-URL fetch; `dangerouslySetInnerHTML`/`v-html` of user data; whole-DB-row as a client prop | | MEDIUM | Real but bounded, or reliability | Personalized SSR page cacheable at a shared CDN; hydration mismatch on user data; missing CSP; module-level SSR state; unpatched non-critical CVE | | LOW | Deviation from SOTA | Pages Router for greenfield; hand-rolled memoization vs React Compiler; `$fetch` double-fetch in setup; Options API for a new app | | INFO | Worth knowing | Newer render mode available; migration opportunities | ### Finding format ``` file:line | rule violated (rules/NN §S) | severity | effort | fix ``` Effort: trivial · small · medium · large. Group by severity, CRITICAL first. Borderline severities state the deciding assumption; unconfirmed findings are marked "needs verification", never asserted. End with per-severity counts, the sweep commands run, and explicit "checked and clean" areas. ## Rules index | File | Read this when... | |---|---| | `rules/01-baseline.md` | choosing or verifying a stack and version floor (React/Next/Vue/Nuxt support+EOL matrix), picking a render mode (CSR/SSR/SSG/ISR/PPR), project setup, React Compiler | | `rules/02-react.md` | writing React components: hooks rules, `useId`/`useEffect`/refs, Suspense & error boundaries, `use()`/Actions/`useActionState`, memoization & the React Compiler, `dangerouslySetInnerHTML` | | `rules/03-nextjs.md` | Next.js App Router: Server vs Client Components, Server Actions, the caching model (`use cache`/Cache Components/PPR/ISR/`revalidate`), `proxy.ts`/middleware, the Data Access Layer, and Next CVEs | | `rules/04-vue.md` | writing Vue: Composition API & `<script setup>`, reactivity pitfalls (props destructure, `shallowRef`, watchers, `effectScope`), `defineModel`, TypeScript, and Vue XSS (`v-html`, template injection) | | `rules/05-nuxt.md` | Nuxt 4: data fetching (`useFetch`/`useAsyncData`/`$fetch`), `useState`, `runtimeConfig`, Nitro server routes & auth, `routeRules`/hybrid rendering, islands, and Nuxt/Nitro/h3/IPX CVEs | | `rules/06-ssr-hydration.md` | anything SSR: hydration mismatches & determinism, SSR state-serialization XSS, cross-request state pollution, caching personalized pages safely, CSP with streaming SSR | | `rules/07-security.md` | a security pass: the server/client secret boundary, authorization placement (post-CVE-2025-29927), SSRF surfaces, the consolidated framework CVE reference, and supply-chain notes | ## Top-10 non-negotiables 1. **Run supported, patched majors.** React ≥ 19.x, Next ≥ 15.x (16.x current), Vue ≥ 3.5, Nuxt ≥ 4.x (Nuxt 3 security-only until 2026-07-31). Cross-check the CVE tables — an unpatched RSC RCE (CVE-2025-55182) or middleware bypass (CVE-2025-29927) is a ship-stopper. (`rules/01`, `rules/03`, `rules/05`) 2. **Authorization lives next to the data, never only in middleware or a layout.** `proxy.ts`/middleware is an optimization, not the auth layer; each Server Action, Route Handler, and Nitro route re-checks authn + authz. (`rules/03`, `rules/07`) 3. **Every prop that crosses server→client is public.** It's serialized into the RSC payload / HTML. Pass minimal DTOs, never raw DB rows, tokens, or `process.env`. (`rules/03`, `rules/07`) 4. **Server Components by default; `"use client"` only where interactivity requires it.** Keep secrets and data access in Server Components / server-only modules (`import 'server-only'`). (`rules/02`, `rules/03`) 5. **Secrets never reach the client bundle.** Nothing sensitive in `NEXT_PUBLIC_*`, `VITE_*`, or `runtimeConfig.public` — those are inlined at build time. (`rules/07`) 6. **Validate every server-route/action input with a schema** (`getValidatedBody`/Zod; never trust `formData`, `searchParams`, or headers), and check resource ownership to prevent IDOR. (`rules/03`, `rules/05`) 7. **Hydrate deterministically:** no `Date`/random/`window` in render, stable IDs via `useId`, SSR-safe shared state — and never "fix" a mismatch by injecting server-side user HTML. (`rules/06`) 8. **Serialize SSR state safely.** Framework serializer (devalue) or `<`-escaped JSON in `<script>`; naked `JSON.stringify` into a script tag is XSS. (`rules/06`) 9. **User HTML is dangerous HTML.** `dangerouslySetInnerHTML`/`v-html` only on sanitizer output; validate `href`/URL schemes (`javascript:`); lock `next/image` `remotePatterns` to explicit hosts (no `**`). (`rules/02`, `rules/04`, `rules/07`) 10. **Don't cache personalized SSR at a shared cache.** `Cache-Control: private` for per-user pages; know that CDNs drop `Vary`; wire CSP nonces knowing they force dynamic rendering. (`rules/06`)
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.