nuxt-fullstack-security-review
Statically review Nuxt 3/4 full-stack code for private secrets exposed via runtimeConfig.public/NUXT_PUBLIC_* env vars, useState/module-scope cross-request state pollution in Nitro, server-route SSRF via $fetch/ofetch with blind useRequestHeaders/credential forwarding, NuxtPayloa
Install
npx skills add https://github.com/VincentChuWaiChow/vanguard-frontier-agentic/tree/master/skills/frontend/nuxt-fullstack-security-review
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install vincentchuwaichow-vanguard-frontier-agentic@llmmart
git clone https://github.com/VincentChuWaiChow/vanguard-frontier-agentic.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole vincentchuwaichow/vanguard-frontier-agentic collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Nuxt Fullstack Security Review
Purpose
Review Nuxt 3/4 full-stack code — nuxt.config.ts, composables/, plugins/,
server/api/, server/routes/, server/middleware/, and templates using
useState/v-html — for five defect classes Nuxt's own documentation and its
long-lived Nitro server model make security-critical: (a) private secrets placed
under runtimeConfig.public (or fed by NUXT_PUBLIC_* env vars) so they ship in the
client bundle, (b) useState/module-scope reactive or mutable state leaking across
requests in Nitro's single long-lived process, (c) server-route SSRF via
$fetch/ofetch to a user-controlled URL and blind forwarding of
useRequestHeaders()/credentials, (d) NuxtPayload/useState data reaching an
unsanitized render sink (XSS), and (e) missing security response headers
(routeRules headers, the nuxt-security module, useResponseHeader). This
skill exists so the review stays anchored to these five documented, structural
defect classes instead of drifting into a general "Nuxt code review."
When to use
Use this skill when the user asks to:
- review a Nuxt
nuxt.config.tsruntimeConfig/routeRulesblock for secret-exposure or missing-header risk, - review a
server/api/*orserver/routes/*handler that calls out to another service ($fetch/ofetch/event.$fetch), - investigate a report of one user seeing another user's data from a Nuxt app — the classic cross-request state pollution symptom,
- assess whether a
useState/payload value rendered withv-htmlis safe, - perform a pre-launch security review of a Nuxt 3/4 full-stack application.
Do not use this skill for:
- a Nuxt app's client-only component architecture, composable-extraction quality, or
reactivity-boundary design with no security angle — use
vue-composition-api-architecture-reviewinstead, - general Vue SSR concerns (non-Nuxt
entry-server.js, raw@vue/server-rendererusage) with no Nuxt-specific API involved — usevue-ssr-security-reviewinstead, - Vuex/Pinia store internals or Vue Router navigation-guard security with no
Nuxt-specific
runtimeConfig/server//useStatesurface — usevue-state-store-security-revieworvue-router-navigation-security-reviewinstead, - a bug that requires live traffic reproduction (concurrent-request load testing, a captured cross-user response, an actual SSRF probe against a running deployment) to confirm exploitation — static analysis proves the structural risk, not that it has already been exploited in production.
Context7 Documentation Protocol
- Resolve and query
/websites/nuxt_4_x(primary; Nuxt 4 prose docs) and/websites/nuxt_3_x(Nuxt 3 prose docs) before citing anyruntimeConfig,useState,$fetch/event.$fetch/useRequestFetch/useRequestHeaders, payload/devalue,routeRules, oruseResponseHeaderbehavior as fact. Both are Nuxt's own documentation site content mirrored into Context7 — treat matches from either asdocumentation-based. - Confirm which Nuxt major the target repo uses (
package.json'snuxtdependency) before assuming version-specific defaults; the APIs this skill covers are stable across 3/4, but state which major was confirmed when citing a claim. - The third-party
nuxt-securitymodule's exact default header set and configuration surface is not covered by Nuxt's own Context7-indexed docs — never state a specific default for it asdocumentation-based. Confirm its presence/config by reading the repo'snuxt.config.tsdirectly, and label any claim about its behaviorinferenceunless corroborated by the module's own documentation (not currently in scope for this skill's Context7 grounding). - Do not invent API names. If Context7 does not confirm an API or default, say so
explicitly and label the claim
inference. - If Context7 is unavailable, fall back to the
official_docsURLs in this skill'smetadata.jsonand label the claimdocumentation-based, unverified against current release.
Lean operating rules
- Every finding in this skill's five defect classes defaults to HIGH severity
(missing-security-headers may be MEDIUM-to-HIGH depending on the app's actual
surface — see
references/ssrf-payload-and-response-headers.md, Part 3). Do not downgrade a structuralruntimeConfigexposure, cross-request state leak, SSRF path, or payload-XSS trace to informational because it has not been observed exploited yet. - Trace every finding to a concrete file:line and a concrete data-flow path. A finding that says "this might leak the secret" or "this fetch call could be SSRF" without showing the specific config key, the specific module-scope declaration and its reachability, or the specific origin-to-sink trace is a guess, not a finding.
- Classify every
runtimeConfigkey by its actual nesting (top-level = private, underpublic= client-exposed) — never by variable name alone. A key namedapiSecretsitting insidepublicis exposed; a key namedbaseUrlsitting outsidepublicis still private and not itself a finding. - Classify every module-scope declaration on two axes before flagging it:
mutability/reactivity (only
useState/ref/reactive/mutable objects are at risk; immutable constants are not), and reachability from server-rendered code (a declaration no server-rendered path ever touches is not a finding in this scope). - Do not clear a
$fetch/ofetch/event.$fetchcall in a server route as safe from SSRF just because it "looks like an API call" — trace the destination URL to its origin and confirm either a hardcoded host or an explicit allowlist check before the request fires. - Do not clear a header-forwarding call (
event.$fetch's default forwarding,useRequestHeaders(...), or manual header spreading) as safe just because Nuxt documents the mechanism — the mechanism existing is not the same as its use being scoped to only the headers actually needed and only trusted destinations. - Do not approve a
useState/payload value reaching av-htmlbinding unless a named sanitizer call is visibly present on that exact traced path — a sanitizer existing elsewhere in the codebase does not clear this bar. - Do not report "no security headers" as cleared just because a mechanism
exists somewhere in the config — confirm the
routeRulesglob (or module config) actually covers the routes in scope before crediting it. - Never execute, build, or run application code, and never send live requests, as part of this review; this is a static-review skill (Read/Grep/Glob only).
- Load only the reference needed for the concern in scope.
References
Load these only when needed:
- Review workflow and findings contract — use for the step-by-step review procedure, the full decision tree across all five defect classes, and the required output shape.
- runtimeConfig exposure and cross-request state pollution
— load when reviewing
nuxt.config.ts'sruntimeConfigblock,NUXT_PUBLIC_*env vars, oruseState/module-scope reactive/mutable declarations reachable from server-rendered code. - Server-route SSRF, header forwarding, payload XSS, and missing response headers
— load when reviewing a
server/api/server/routeshandler's outbound$fetch/ofetch/event.$fetch/useRequestFetchcalls, auseState/payload value that renders somewhere, orrouteRules/security-module configuration. - Acceptance rubric — the authoritative list of defects this skill must catch and the false positives it must not raise; consult when unsure whether a pattern is in scope.
Response minimum
Return, at minimum:
- the
runtimeConfig/routeRulesblocks, module-scope declarations, server routes, and/or template bindings in scope, - ranked findings with file:line evidence, defect category
(
runtimeconfig-exposure/cross-request-state-pollution/ssrf/credential-forwarding/payload-xss/missing-security-headers), the concrete data-flow trace, and a fix sketch matching Nuxt's documented pattern, - for every
useState/payload → render-sink finding, an explicit statement of whether a sanitizer call is present on the traced path — never approve on the assumption one exists elsewhere, - evidence level per finding (
repo evidence,documentation-based, orinference), with structural risk findings explicitly labeled as structural risk, not as confirmed-exploited, - verdict (approve / approve-with-notes / block),
- open questions or scope the review could not cover (e.g., "confirming actual cross-request leakage requires concurrent-request load testing, not static review").
Files (vanguard-frontier-agentic)
-
references
-
acceptance-rubric.md 6.5 KB
# Acceptance Rubric (write this first — it is the failing test) This rubric is the spec for `nuxt-fullstack-security-review`. Every item below must be covered by an explicit operating rule, decision-tree branch, or reference section before this skill ships. `SKILL.md` and the other `references/*.md` files are written to satisfy this list, not the other way around. ## MUST catch (true positives) 1. **Private secret placed under `runtimeConfig.public.*`.** A key holding a secret (API key, DB credential, signing secret, third-party token) declared inside the `public` block of `runtimeConfig` in `nuxt.config.ts` — or a `NUXT_PUBLIC_*` environment variable feeding one — ships to the client JS bundle. HIGH. Evidence: Context7 `/websites/nuxt_4_x` confirms only `runtimeConfig.public.*` (and `runtimeConfig.app.*`) are exposed to the client; every other key is server-only, and only an uppercase `NUXT_`-prefixed env var overrides a *matching* runtimeConfig key at runtime. 2. **Private secret with a name/shape suggesting sensitivity kept at top-level `runtimeConfig` but actually read from a public-scoped variable, or accidentally duplicated into `public` "for convenience" (e.g., a client composable needs the same value).** Still HIGH — the fix is a server-only proxy endpoint, not exporting the secret to `public`. 3. **`useState` or module-scope `ref()`/`reactive()`/mutable object declared outside any composable/component function body**, used to hold per-user or per-request data (session, cart, auth token, request-specific query results) in server-rendered Nitro code. Because Nitro is a single long-lived process serving concurrent requests, this is cross-request state pollution — HIGH, structural, regardless of whether it has been observed leaking. 4. **A composable that *looks* per-request (invoked inside `setup()`/an event handler) but closes over a shared module-level cache/singleton/mutable default parameter**, defeating the apparent isolation. HIGH. 5. **`server/api/*` route (`defineEventHandler`) calling `$fetch`/`ofetch` with a URL built from user-controlled input (route param, query string, request body) with no host allowlist** — classic SSRF: an attacker can redirect the server's outbound request to an internal address or arbitrary host. HIGH. 6. **A server route reading `useRequestHeaders()` (or forwarding `event.node.req.headers` directly) and blindly forwarding all headers — or specifically `authorization`/ `cookie` — to a third-party/user-controlled outbound `$fetch` call** without an allowlist of headers and without restricting the destination host. HIGH — this leaks the current user's credentials to whatever host the URL resolves to. 7. **`useState`/payload data containing user-controlled/echoed content (a query param, a comment body, a profile field) serialized into the SSR payload and then rendered unescaped** (e.g., interpolated with `v-html`, or written into an inline `<script>`/`<style>` block by custom server middleware bypassing Nuxt's own `devalue`-based payload serialization) → XSS. HIGH. (Nuxt's own payload serialization via `devalue` is not itself an XSS sink; the finding is about *what renders that payload data unescaped afterward* or about a hand-rolled payload/script-injection path that bypasses `devalue`.) 8. **No security response headers configured anywhere** (no `routeRules` `headers` entries, no `nuxt-security` module, no middleware calling `useResponseHeader`) for an app that serves authenticated pages, handles forms, or embeds third-party content — missing CSP/X-Frame-Options/X-Content-Type-Options is a MEDIUM-to-HIGH finding depending on what the app does (a public informational-only site with no auth and no user input is lower severity than a logged-in dashboard). ## MUST NOT flag (false positives to actively avoid) 9. A value correctly declared as a **private** `runtimeConfig` key (outside `public`), even if consumed only server-side in `server/api/*` — this is the *correct* pattern and must not be flagged just because "secrets in config feel risky." 10. `runtimeConfig.public.*` holding genuinely non-secret data (a base API URL, a feature flag, a public analytics ID) — public-by-design values are not findings. 11. `useState`/`ref`/`reactive` declared **inside** a composable function, component `setup()`, or plugin factory that runs per-request/per-component-instance — this is the documented, safe pattern; do not flag every `useState` call site. 12. A module-scope `const` holding an **immutable** value (a frozen route table, a static config object, a compiled regex, a constant lookup map) with no runtime mutation path — immutable module-scope constants are safe to share across requests. 13. A `server/api/*` route whose `$fetch`/`ofetch` target is a **hardcoded or allowlisted** host (e.g., validated against a fixed list of trusted upstream domains, or the URL is entirely server-config-derived with no user input in the host/path) — do not flag SSRF when the destination is not attacker-influenceable. 14. `event.$fetch(...)` used deliberately to forward request context to another **internal** Nitro route for legitimate context propagation, with no external host involved — not an SSRF/credential-leak finding by itself; only flag when the *destination* is external/user-controlled or when *sensitive* headers are forwarded to it without justification. 15. An app that already sets a documented security header mechanism (an explicit `routeRules` `headers` block with CSP/HSTS/etc., or the `nuxt-security` module configured) — do not flag "missing headers" when a header-setting mechanism is present and covers the routes in scope; only flag genuine gaps (e.g., headers set for `/` but not for `/admin/**`). ## Evidence discipline - Every finding must cite file:line and a concrete data-flow trace (declaration → reachability, or origin → sink). - Every framework-behavior claim in the finding write-up must be labeled `documentation-based` (Context7-confirmed against `/websites/nuxt_4_x` or `/websites/nuxt_3_x`), `repo evidence` (observed directly in the file under review), or `inference` (reasonable extrapolation not directly stated in either source — e.g., community conventions like the third-party `nuxt-security` module's exact default header set, which Context7's Nuxt-core docs do not themselves enumerate). - Do not invent API names. If Context7 does not confirm an API or default, say so and label the claim `inference`. -
runtime-config-and-cross-request-state.md 8.6 KB
# runtimeConfig Exposure and Cross-Request State Pollution Use this reference when the review scope includes `nuxt.config.ts`'s `runtimeConfig` block, any `.env`/`NUXT_*` variable naming, or `useState`/module-scope reactive declarations reachable from server-rendered code. ## Part 1 — runtimeConfig: private vs public split ### What people get wrong The naive assumption is: > "I put it in `runtimeConfig`, so it's server-only." Wrong — only *part* of `runtimeConfig` is server-only. Nuxt's own config API distinguishes two zones inside the same object: ```ts export default defineNuxtConfig({ runtimeConfig: { // Private keys are only available on the server apiSecret: '123', // Public keys that are exposed to the client public: { apiBase: process.env.NUXT_PUBLIC_API_BASE || '/api', }, }, }) ``` (`documentation-based`, Context7 `/websites/nuxt_4_x`, `useRuntimeConfig`/ `nuxt.config` API reference and the runtime-config guide.) Everything nested under the `public` key (and, per the same source, the `app` key) is serialized into the client bundle and readable by anyone who opens dev tools. Everything else in `runtimeConfig` stays server-only. ### The environment-variable trap Context7 confirms two hard rules for env-var overrides (`documentation-based`, `/websites/nuxt_4_x`, "Runtime Config > Exposing > Environment Variables" and the migration guide): 1. Only variables **already declared** in `nuxt.config`'s `runtimeConfig` can be overridden at runtime — arbitrary env vars are never auto-exposed. 2. Only an **uppercase environment variable prefixed `NUXT_`**, using `_` to separate nested keys, overrides the matching config path. A `NUXT_PUBLIC_*` variable maps to a key under `public`; a `NUXT_*` variable without `PUBLIC` maps to a private top-level key. The security-relevant consequence: a developer who names an env var `NUXT_PUBLIC_API_SECRET` — intending it to *feel* private because of the word "SECRET" — has just told Nuxt to expose it to the client, because the naming convention that controls exposure is structural (`public.*` nesting / the `PUBLIC` segment), not the variable's English-language name. ### Verification targets - Grep `nuxt.config.*` for `runtimeConfig:` and read the full block, including the nested `public: { ... }` object. - For every key under `public`, ask: does its name or the value it defaults to (`process.env.*`) suggest a secret, credential, signing key, or internal hostname? If yes → HIGH finding, move it out of `public` (or behind a server-only proxy endpoint that reads the private key and returns only what the client needs). - For every key **outside** `public` (private), confirm it is never re-exported into a public-scoped value elsewhere (e.g., a plugin or composable doing `useRuntimeConfig().public.foo = useRuntimeConfig().apiSecret` — an anti-pattern that defeats the private/public split at runtime). - Grep `.env`, `.env.*`, and deployment/CI config for `NUXT_PUBLIC_*` variable names; confirm each maps to a genuinely public value in `nuxt.config`. - Do not flag a private key merely for existing — that is the correct, safe pattern (rubric item 9). Only flag actual public-zone exposure or private→public leakage. ### Fix sketch ```ts // nuxt.config.ts — correct export default defineNuxtConfig({ runtimeConfig: { apiSecret: '', // NUXT_API_SECRET — server-only, never reaches the client public: { apiBase: '', // NUXT_PUBLIC_API_BASE — fine to expose }, }, }) ``` ```ts // server/api/proxy.ts — client needs data derived from the secret, // so it calls a server route, never the secret itself export default defineEventHandler(async (event) => { const config = useRuntimeConfig(event) const result = await $fetch('https://upstream.example.com/data', { headers: { Authorization: `Bearer ${config.apiSecret}` }, }) return result // only the derived, non-secret payload leaves the server }) ``` ## Part 2 — useState / module-scope cross-request state pollution ### What people get wrong The naive assumption is: > "Nuxt handles SSR for me, so any state I declare is automatically per-request." Wrong. Nitro (Nuxt's server engine) is a single long-lived Node.js process handling many concurrent requests. Context7 confirms the exact failure mode directly (`documentation-based`, `/websites/nuxt_4_x`, "Auto-imports > Built-in Auto-imports"): Vue/Nuxt tracks the current component instance (and Nuxt's own `nuxtApp`) via a transient global reference specifically to avoid "cross-request state pollution (leaking a shared reference between two users)." That same guide's State Management page states the rule as a best practice directly: never define `const state = ref()` outside of `<script setup>` or a `setup()` function — doing `export myState = ref({})` "would result in state shared across requests on the server and can lead to memory leaks." (`documentation-based`, `/websites/nuxt_4_x`, "State Management > Best Practices.") `useState` itself is the documented safe replacement: an "SSR-friendly `ref` replacement" whose "value will be preserved after server-side rendering (during client-side hydration) and shared across all components using a unique key" (`documentation-based`, same source) — but `useState` is only safe when invoked inside a component/composable/plugin function body, where Nuxt's per-request context tracking scopes it correctly. Calling `useState` (or any `ref`/`reactive`) at true module scope defeats that scoping. ### Non-negotiable design rules 1. **Classify every module-scope declaration by mutability/reactivity, then by reachability** — same two-axis test as any SSR framework: is it `ref()`/`reactive()`/`useState()`/a mutable object or array literal (risk), or an immutable constant (safe)? Is it imported/read/written by any server-rendered composable, plugin, or `server/api` handler (reachable), or truly dead/unused (not a finding in this scope)? 2. **A composable wrapper does not automatically fix it.** `const useX = () => useState('x')` is the documented safe pattern *only* because `useState`'s internal key-based lookup is itself scoped per Nuxt-app-instance/per-request. A hand-rolled module-scope cache object that a composable merely *reads* from (rather than routing through `useState`/a genuinely per-request store) is not fixed by wrapping the read in a function — trace what the function actually touches, not just its outer shape. 3. **Server-side (`server/api`, `server/middleware`) module-scope mutable state is just as much at risk as client/universal composable state** — Nitro's event handlers run in the same long-lived process. A module-level `let cache = {}` written to inside a `defineEventHandler` and read by a later request from a different user is a textbook cross-request leak, independent of Vue/useState entirely. 4. **Closures over shared mutable state defeat an otherwise-correct factory** — a `defineEventHandler` that looks correct because it doesn't declare state at module scope can still close over a module-level mutable variable from its enclosing file. ### Verification targets - Grep for `useState(` calls and confirm each is inside a `<script setup>` block, `setup()` function, composable function body, or Nuxt plugin factory — not at a file's top level. - Grep for `ref(`/`reactive(` outside any function body across `composables/`, `plugins/`, `server/api/`, `server/middleware/`, and any file imported by them. - Grep `server/` for `let `/mutable `const {}`/`const []` declarations at module scope; check whether any `defineEventHandler` in the same file (or importing the module) reads or writes them. - For each hit, trace reachability: is it imported by any server-rendered route, composable, or middleware that runs per-request? If no server-rendered code path reaches it, it is not a finding in this review's scope (rubric item 12 for immutable data; otherwise flag as dead code, not a security finding). ### Fix sketch ```ts // BAD — server/api/session.ts: module-scope mutable object shared across all requests const lastUser: Record<string, unknown> = {} export default defineEventHandler((event) => { lastUser.id = getQuery(event).userId // every concurrent request writes the SAME object return lastUser }) ``` ```ts // GOOD — no shared mutable state; everything is derived fresh per request export default defineEventHandler((event) => { const userId = getQuery(event).userId return { id: userId } }) ``` ```ts // GOOD — universal/component state via useState, invoked inside a composable export const useCounter = () => useState('counter', () => 0) ``` -
ssrf-payload-and-response-headers.md 13 KB
# Server-Route SSRF, Header Forwarding, Payload XSS, and Missing Response Headers Use this reference when the review scope includes a `server/api/*` or `server/routes/*` event handler (`defineEventHandler`), any `$fetch`/`ofetch`/ `event.$fetch`/`useRequestFetch` call inside server code, a `useState`/payload value that renders into a template, or `nuxt.config.ts`'s `routeRules`. ## Part 1 — server route SSRF via $fetch/ofetch + header forwarding ### What people get wrong The naive assumption is: > "It's just a server-side fetch, not user input rendered in the browser, so it > can't be a security issue." Wrong on two counts. First, if the **target URL** of a server-side `$fetch` call is built from user-controlled input (a route param, query string, or request body) with no allowlist, the server itself becomes an attacker-controlled HTTP client — a Server-Side Request Forgery (SSRF) primitive that can reach internal network addresses, cloud metadata endpoints, or arbitrary hosts the attacker chooses. Second, Nuxt's own docs draw a sharp, explicit line around **header forwarding** that is easy to miss: - Bare `$fetch(...)` inside a server route does **not** forward the incoming request's headers or context by default (`documentation-based`, `/websites/nuxt_4_x`, "Forwarding Context & Headers": "By default, neither the headers from the incoming request nor the request context are forwarded when making fetch requests in server routes.") - `event.$fetch(...)` is the documented way to forward the request context and headers (`documentation-based`, same source): `export default defineEventHandler((event) => { return event.$fetch('/api/forwarded') })`. - `useRequestFetch()` is the documented composable for explicitly forwarding the current user's headers and cookies during SSR when plain `$fetch` would not include them (`documentation-based`, `/websites/nuxt_4_x`, `useRequestFetch` API reference). - Nuxt's data-fetching guide states the caution directly: "Exercise caution when proxying headers to external APIs, only including those that are strictly necessary. Headers like 'host', 'accept', 'content-length', 'content-type', and various 'x-forwarded' or 'cf-' headers should generally not be proxied." (`documentation-based`, `/websites/nuxt_4_x`, "$fetch > Pass Client Headers to the API.") - `useRequestHeaders(['authorization'])` is documented specifically to proxy the `authorization` header to an internal isomorphic `$fetch` call during SSR (`documentation-based`, `/websites/nuxt_3_x`, `useRequestHeaders` API reference, "Proxying Authorization Header in SSR"). The security-relevant synthesis: **there is no built-in URL validation on `$fetch`/`ofetch`/`event.$fetch` targets**, and **there is no built-in header allowlist** on what `event.$fetch`/`useRequestFetch`/manually-forwarded headers send onward. Both are the calling code's responsibility. A route that (a) builds its outbound URL from user input and (b) forwards the current user's `authorization`/`cookie` header to that user-influenceable destination combines SSRF with credential exfiltration. ### Non-negotiable design rules 1. **Trace the outbound URL to its origin.** For every `$fetch`/`ofetch`/ `event.$fetch`/`useRequestFetch` call in a `server/api`/`server/routes` handler, find where the URL string comes from. If any segment (host, path, or query) is built from `getQuery(event)`, `getRouterParam(event, ...)`, `readBody(event)`, or an equivalent user-reachable source, with no allowlist check against a fixed set of trusted hosts before the call — HIGH SSRF finding. 2. **A hardcoded or allowlisted host is not a finding.** If the destination host is a literal string, an env-config value with no user input in it, or the user-supplied portion is validated against an explicit allowlist of trusted hosts before the request fires, do not flag it (rubric item 13). 3. **Trace what headers actually get forwarded, and to where.** `event.$fetch` forwards request context/headers by default — check whether the destination is internal (same-origin Nitro route) or external. Forwarding to an internal route with no external network hop is not a credential-leak finding by itself (rubric item 14). Forwarding `authorization`/`cookie` to an external or user-influenceable host — with no allowlist limiting which headers cross that boundary — is HIGH. 2b. **Manual, unfiltered header spreading is worse than `event.$fetch`.** Code that reads `event.node.req.headers` (or `getHeaders(event)`) and spreads the entire object into an outbound `$fetch`'s `headers` option forwards everything, including headers Nuxt's own docs say should generally not be proxied (`host`, `accept`, `content-length`, `content-type`, `x-forwarded-*`, `cf-*`) — flag this even before considering the destination, since it also risks breaking the outbound request's own framing/routing semantics, and flag it as HIGH if any sensitive header (`authorization`, `cookie`) is in the spread and the destination is external or user-influenceable. ### Verification targets - Grep `server/api/` and `server/routes/` for `$fetch(`, `ofetch(`, `event.$fetch(`, `useRequestFetch(`, `useRequestHeaders(`, `getHeaders(`. - For each `$fetch`-family call, read backward to the URL argument's full construction; flag string concatenation/template literals combining a fixed base with a user-reachable variable, or a bare user-supplied URL. - For each `useRequestHeaders(...)` or `getHeaders(...)` call, check what keys are requested/forwarded and where the result is used downstream. ### Fix sketch ```ts // BAD — server/api/proxy.ts: user controls the entire outbound host export default defineEventHandler(async (event) => { const { url } = getQuery(event) return $fetch(url as string) // SSRF: attacker can point this at internal services }) ``` ```ts // GOOD — allowlisted upstream host, no user control over destination const ALLOWED_HOSTS = new Set(['api.trusted-partner.com']) export default defineEventHandler(async (event) => { const { path } = getQuery(event) const target = new URL(String(path), 'https://api.trusted-partner.com') if (!ALLOWED_HOSTS.has(target.host)) { throw createError({ statusCode: 400, statusMessage: 'Invalid target' }) } return $fetch(target.toString()) }) ``` ## Part 2 — NuxtPayload / useState serialization into rendered HTML ### What people get wrong The naive assumption is: > "Nuxt serializes my state into the payload automatically, so whatever I put in > `useState` is handled safely by the framework." Partially wrong. Nuxt 3/4's payload mechanism (`nuxtApp.payload`, covering `data` from `useFetch`/`useAsyncData` and `state` from `useState`) is serialized for transfer from server to client using `devalue`, which supports "advanced data types beyond basic JSON, such as Dates, Maps, Sets, refs, reactives, and NuxtErrors" (`documentation-based`, `/websites/nuxt_3_x`, "Serializing Data From Server to Client" / `useNuxtApp` payload reference). `devalue`-based serialization itself is not an HTML-injection sink — it produces a JS expression, not raw HTML concatenation. The actual risk this skill flags is **what happens after** payload data is revived on the client: - If a `useState`/payload value holding user-controlled content (an echoed query param, a comment body, a profile field from an API that itself echoes other users' submissions) is later rendered with `v-html` anywhere in the app, that is a standard unsanitized-`v-html` XSS finding — the payload is simply the transport, not the sink. Trace the value from `useState`/payload through to its eventual render, exactly as in an XSS review of any Vue app. - A custom payload plugin (`definePayloadReducer`/`definePayloadReviver`, documented in `/websites/nuxt_3_x`'s `useNuxtApp` reference) that hand-builds a serialized representation without going through `devalue` — or any custom server middleware that writes directly into the rendered HTML's inline `<script>` tag instead of relying on Nuxt's own payload injection — is a distinct, higher-risk pattern: it bypasses the framework's serialization path entirely and must be checked for proper escaping by hand (`inference`: Nuxt's docs describe the reducer/reviver extension point but do not themselves discuss its injection risk, so grade this claim as inference, not documentation-based). ### Verification targets - Grep for `useState(` call sites; for each, trace the value's origin (is it seeded from `getQuery`, `readBody`, or an API response that echoes user-submitted content?) and its eventual consumers (any `v-html` binding, any `innerHTML`/`dangerouslySetInnerHTML`-equivalent sink). - Grep for `definePayloadReducer(`/`definePayloadReviver(` and read the custom serialization logic for any manual string concatenation that lands in rendered HTML. - Apply the same sanitizer-on-exact-path standard as any `v-html` review: a sanitizer existing elsewhere in the codebase does not clear a specific traced path (same principle as Vue's own `v-html` guidance). ### Fix sketch ```vue <script setup> import DOMPurify from 'dompurify' // comment.body is user-submitted content that round-tripped through // useAsyncData's payload cache — the payload is the transport, this // sanitizer call is what actually clears the finding. const { data: comment } = await useAsyncData('comment', () => $fetch(`/api/comments/${id}`)) const safeBody = computed(() => DOMPurify.sanitize(comment.value?.body ?? '')) </script> <template> <div v-html="safeBody" /> </template> ``` ## Part 3 — missing security response headers ### What people get wrong The naive assumption is: > "Nuxt is a modern framework, so it must ship secure headers (CSP, X-Frame-Options, > etc.) by default." Wrong. Context7 confirms the mechanisms available, but not that any are enabled by default: - `routeRules` in `nuxt.config.ts` supports a `headers` property that "allows adding specific HTTP headers to sections of your site" (`documentation-based`, `/websites/nuxt_4_x`, "Hybrid Rendering > Route Rules"), and a documented example shows `cors: true` adding CORS headers to an API route glob, further customizable via the same `headers` property. Nothing in the confirmed docs shows CSP/X-Frame-Options/HSTS enabled by default — these must be configured explicitly per route or globally (`'/**': { headers: { ... } }`). - `useResponseHeader(name)` is a documented composable for setting any server response header from within pages, components, or plugins, including on a per-page basis (`documentation-based`, `/websites/nuxt_4_x`, `useResponseHeader` API reference). - A dedicated community security module (commonly referred to as `nuxt-security`) exists to apply a curated default header set (CSP, HSTS, X-Frame-Options, and related hardening) in one step. Context7's Nuxt-core documentation set (`/websites/nuxt_4_x`, `/websites/nuxt_3_x`) does not itself document this third-party module's API or defaults — treat any specific claim about its default header values as `inference`, not `documentation-based`; confirm its presence/config directly in the repo (`nuxt.config.ts` `modules` array and any `security:` config block) rather than asserting what it does from memory. ### Non-negotiable design rules 1. **Absence of any header-setting mechanism is the finding, not the absence of a specific module.** Check for *any* of: a `routeRules` block with a `headers` entry covering the routes in scope, a `nuxt-security`-style module in the `modules` array, or middleware/plugin code calling `useResponseHeader(...)`. If none exist anywhere in the app, and the app handles authentication, forms, or third-party embeds, flag MEDIUM-to-HIGH depending on what the app does (rubric item 8). 2. **Partial coverage is a real, narrower finding.** If headers are set for `/` but not for `/admin/**` or `/api/**`, do not report "headers exist, fine" — report the specific gap, citing the routeRules glob(s) that are covered vs. not. 3. **Do not flag an app that already has a working mechanism covering the routes in scope** (rubric item 15) — verify the glob pattern's actual coverage before crediting it, since a `'/blog/**': { headers: {...} }` rule does not cover `/admin/**` or the site root. ### Verification targets - Grep `nuxt.config.*` for `routeRules` and inspect every entry for a `headers` key; note which route globs are covered. - Grep `nuxt.config.*` `modules` array for a security-module name (e.g., `nuxt-security`) and read its adjacent config block if present. - Grep the codebase for `useResponseHeader(` calls in plugins/middleware/pages. - Cross-reference coverage against the app's actual surface (auth pages, forms, admin routes, embedded third-party widgets) to size the severity. ### Fix sketch ```ts // nuxt.config.ts export default defineNuxtConfig({ routeRules: { '/**': { headers: { 'X-Frame-Options': 'DENY', 'X-Content-Type-Options': 'nosniff', 'Content-Security-Policy': "default-src 'self'", }, }, }, }) ``` -
workflow-and-output.md 7.8 KB
# Review Workflow and Findings Contract Use this reference for the step-by-step review procedure, the decision tree across all five defect classes, and the required output shape. Load the two domain references only for the specific defect class the code under review actually raises. ## Prerequisites - Read `package.json` / `nuxt.config.ts` to confirm the Nuxt major (3 vs 4) — API names and defaults are stable across 3/4 for everything this skill covers, but confirm before citing version-specific behavior. - Identify the review surface: `nuxt.config.ts` (`runtimeConfig`, `routeRules`, `modules`), any `composables/`, `plugins/`, `server/api/`, `server/routes/`, `server/middleware/` files, and any template/component using `useState`/`v-html`. ## Workflow 1. **Read `nuxt.config.ts`'s `runtimeConfig` block in full.** Classify every key as private (top-level, server-only) or public (nested under `public`, or `app`). For every public key, ask whether its name/default value suggests a secret. See `references/runtime-config-and-cross-request-state.md`, Part 1. 2. **Grep for `NUXT_PUBLIC_*` and other `NUXT_*` env var names** in `.env*` files and deployment config; map each back to the `runtimeConfig` key it overrides and confirm the public/private classification holds. 3. **Enumerate module-scope declarations reachable from server-rendered code.** Grep `composables/`, `plugins/`, `server/api/`, `server/middleware/` for `useState(`, `ref(`, `reactive(`, and mutable object/array literals declared outside any function body. Classify each by mutability/reactivity and reachability. See `references/runtime-config-and-cross-request-state.md`, Part 2. 4. **Enumerate every server-route outbound `$fetch`/`ofetch`/`event.$fetch`/ `useRequestFetch` call.** Trace the destination URL to its origin; trace which headers are forwarded (via `event.$fetch`'s default context propagation, `useRequestHeaders(...)`, or manual header spreading) and to where. See `references/ssrf-payload-and-response-headers.md`, Part 1. 5. **Enumerate `useState`/payload values that carry user-controlled or user-echoed content**, and trace them forward to any eventual `v-html`/HTML-injection sink. Separately check for any custom `definePayloadReducer`/`definePayloadReviver` logic that bypasses Nuxt's own `devalue`-based serialization. See `references/ssrf-payload-and-response-headers.md`, Part 2. 6. **Check for a security-header mechanism**: `routeRules` `headers` entries, a security module in `modules`, or `useResponseHeader(...)` calls, and confirm actual route-glob coverage against the app's real surface (auth pages, forms, admin routes, embedded third-party content). See `references/ssrf-payload-and-response-headers.md`, Part 3. 7. **Produce ranked findings** using the output contract below. ## Decision tree - A key under `runtimeConfig.public` (or fed by a `NUXT_PUBLIC_*` env var) holds a secret, credential, or internal-only value → **HIGH**, `runtimeConfig-exposure`. - A private `runtimeConfig` key is re-exported into a public-scoped value elsewhere in the app → **HIGH**, `runtimeConfig-exposure`. - A private `runtimeConfig` key stays private and is consumed only server-side → not a finding (rubric item 9). - `runtimeConfig.public.*` holds a genuinely non-secret value (base URL, feature flag) → not a finding (rubric item 10). - `useState`/`ref`/`reactive`/a mutable object is declared at true module scope (outside any function) and is reachable from server-rendered code → **HIGH**, `cross-request-state-pollution`, structural, regardless of whether leakage has been observed. - Same declaration is invoked inside a composable/`setup()`/plugin factory body → not a finding (rubric item 11), unless that body itself closes over a separate module-scope mutable reference (still HIGH in that case). - Module-scope declaration is an immutable constant with no runtime mutation path → not a finding (rubric item 12). - A `server/api`/`server/routes` handler builds its outbound `$fetch`/`ofetch` URL (host, path, or query) from user-reachable input with no allowlist → **HIGH**, `ssrf`. - Outbound URL is hardcoded or validated against an explicit host allowlist → not a finding (rubric item 13). - A handler forwards `authorization`/`cookie` (via `event.$fetch` defaults, `useRequestHeaders`, or manual spreading) to an external or user-influenceable destination with no header allowlist → **HIGH**, `credential-forwarding`. - `event.$fetch` used to reach an internal Nitro route with no external hop → not a finding by itself (rubric item 14); only escalate if sensitive headers are forwarded without justification even internally. - A `useState`/payload value carrying user-controlled or user-echoed content reaches a `v-html` (or equivalent unescaped) sink with no sanitizer call on the traced path → **HIGH**, `payload-xss`. - Same value's trace terminates at a safe consumer (text interpolation, a sanitizer call on the exact path, or no rendering at all) → not a finding, but state this explicitly rather than omitting the traced value from the review. - No `routeRules` `headers` entry, no security module, and no `useResponseHeader` call anywhere cover a route the app actually serves with auth/forms/embeds → **MEDIUM-to-HIGH** depending on the app's surface, `missing-security-headers`. - A header mechanism exists and its glob coverage includes the routes in scope → not a finding (rubric item 15) — but call out any gap in coverage for routes it does *not* cover. ## Output contract Every response from this skill must return: 1. **Scope** — the `runtimeConfig`/`routeRules` blocks, module-scope declarations, server routes, and/or template bindings reviewed. 2. **Ranked findings** — each with file:line, defect category (`runtimeconfig-exposure` / `cross-request-state-pollution` / `ssrf` / `credential-forwarding` / `payload-xss` / `missing-security-headers`), the concrete data-flow trace (declaration and reachability, or origin-to-sink path), and a fix sketch matching the patterns in the relevant reference. 3. **Evidence level per finding** — `documentation-based` (Context7-confirmed against `/websites/nuxt_4_x` or `/websites/nuxt_3_x`), `repo evidence`, or `inference` (e.g., third-party module defaults Nuxt's own docs do not enumerate). Label structural risk findings as structural risk, not as confirmed-exploited. 4. **Verdict** — approve / approve-with-notes / block. 5. **Open questions or out-of-scope items** — e.g., "confirming actual cross-request leakage requires concurrent-request load testing, not static review," or "the `nuxt-security` module's exact default header set is not documented in Nuxt's own Context7 sources — confirm its configured `security:` block directly rather than assuming defaults." ## When to push back Push back if the user asks to: - approve a `runtimeConfig.public` secret because "it's minified/obfuscated in the bundle anyway" — minification is not encryption; the value is trivially readable in the browser's network/JS panel, - clear a module-scope `useState`/`ref` because "we haven't seen cross-user leakage in production" — this defect class is structural and often invisible until concurrent load exposes it; absence of a reported incident is not evidence of absence, - approve an SSRF-shaped `$fetch` call because "we trust our users" — an allowlist on the destination host is the control, not the trust level of the current user base, - treat "we sanitize elsewhere" as clearing a specific traced `payload`/`useState` → `v-html` path — the sanitizer must be visible on the exact path under review, - skip the security-headers check because "we'll add nuxt-security later" — report the current gap now; a planned future fix is not a mitigating control today.
-
-
metadata.json 2.4 KB
{ "id": "nuxt-fullstack-security-review", "name": "Nuxt Fullstack Security Review", "type": "skill", "provider": "frontend", "harnesses": [ "claude-code", "cursor", "codex", "gemini", "kiro", "other" ], "summary": "Reviews Nuxt 3/4 full-stack code for private secrets exposed via runtimeConfig.public/NUXT_PUBLIC_* env vars, useState/module-scope cross-request state pollution in Nitro, server-route SSRF via $fetch/ofetch with blind useRequestHeaders/credential forwarding, NuxtPayload/useState serialization reaching an XSS sink, and missing security response headers (routeRules headers, the nuxt-security module), grounding claims via Context7 and Nuxt's own documentation.", "source_type": "original", "official_docs": [ "https://nuxt.com/docs/guide/going-further/runtime-config", "https://nuxt.com/docs/getting-started/state-management", "https://nuxt.com/docs/guide/directory-structure/server", "https://nuxt.com/docs/getting-started/data-fetching", "https://nuxt.com/docs/api/composables/use-request-headers", "https://nuxt.com/docs/api/composables/use-response-header", "https://nuxt.com/docs/guide/concepts/rendering", "https://owasp.org/www-community/attacks/Server_Side_Request_Forgery", "https://owasp.org/www-community/attacks/xss/" ], "security_notes": "This skill's entire scope is security-critical: a runtimeConfig.public/NUXT_PUBLIC_* secret is a client-bundle credential leak, useState/module-scope pollution in Nitro is a cross-tenant/cross-user data-exposure defect, server-route SSRF and blind header forwarding can leak credentials or reach internal network targets, payload/useState reaching an unsanitized sink is XSS, and missing security response headers weakens the app's baseline browser-side defenses. Every finding in this skill defaults to HIGH severity unless proven otherwise with concrete evidence (a private key correctly scoped, an allowlisted outbound host, a sanitizer visibly on the traced path, or documented header coverage of the routes in scope). Static-review-only skill: it reads and greps nuxt.config.ts, composables/plugins/server code, and templates but never executes, builds, or runs application code, and never sends live requests.", "last_verified": "2026-07-03", "path": "skills/frontend/nuxt-fullstack-security-review", "author": "github: VincentChuWaiChow", "version": "0.1.0" } -
SKILL.md 9.4 KB
--- name: nuxt-fullstack-security-review description: Statically review Nuxt 3/4 full-stack code for private secrets exposed via runtimeConfig.public/NUXT_PUBLIC_* env vars, useState/module-scope cross-request state pollution in Nitro, server-route SSRF via $fetch/ofetch with blind useRequestHeaders/credential forwarding, NuxtPayload/useState serialization reaching an XSS sink, and missing security response headers (routeRules headers, nuxt-security), grounded in Nuxt's own documentation via Context7. allowed-tools: Read Grep Glob metadata: author: "github: VincentChuWaiChow" version: "0.1.0" updated: "2026-07-03" category: security --- # Nuxt Fullstack Security Review ## Purpose Review Nuxt 3/4 full-stack code — `nuxt.config.ts`, `composables/`, `plugins/`, `server/api/`, `server/routes/`, `server/middleware/`, and templates using `useState`/`v-html` — for five defect classes Nuxt's own documentation and its long-lived Nitro server model make security-critical: (a) private secrets placed under `runtimeConfig.public` (or fed by `NUXT_PUBLIC_*` env vars) so they ship in the client bundle, (b) `useState`/module-scope reactive or mutable state leaking across requests in Nitro's single long-lived process, (c) server-route SSRF via `$fetch`/`ofetch` to a user-controlled URL and blind forwarding of `useRequestHeaders()`/credentials, (d) `NuxtPayload`/`useState` data reaching an unsanitized render sink (XSS), and (e) missing security response headers (`routeRules` `headers`, the `nuxt-security` module, `useResponseHeader`). This skill exists so the review stays anchored to these five documented, structural defect classes instead of drifting into a general "Nuxt code review." ## When to use Use this skill when the user asks to: - review a Nuxt `nuxt.config.ts` `runtimeConfig`/`routeRules` block for secret-exposure or missing-header risk, - review a `server/api/*` or `server/routes/*` handler that calls out to another service (`$fetch`/`ofetch`/`event.$fetch`), - investigate a report of one user seeing another user's data from a Nuxt app — the classic cross-request state pollution symptom, - assess whether a `useState`/payload value rendered with `v-html` is safe, - perform a pre-launch security review of a Nuxt 3/4 full-stack application. Do not use this skill for: - a Nuxt app's client-only component architecture, composable-extraction quality, or reactivity-boundary design with no security angle — use `vue-composition-api-architecture-review` instead, - general Vue SSR concerns (non-Nuxt `entry-server.js`, raw `@vue/server-renderer` usage) with no Nuxt-specific API involved — use `vue-ssr-security-review` instead, - Vuex/Pinia store internals or Vue Router navigation-guard security with no Nuxt-specific `runtimeConfig`/`server/`/`useState` surface — use `vue-state-store-security-review` or `vue-router-navigation-security-review` instead, - a bug that requires live traffic reproduction (concurrent-request load testing, a captured cross-user response, an actual SSRF probe against a running deployment) to confirm exploitation — static analysis proves the structural risk, not that it has already been exploited in production. ## Context7 Documentation Protocol - Resolve and query `/websites/nuxt_4_x` (primary; Nuxt 4 prose docs) and `/websites/nuxt_3_x` (Nuxt 3 prose docs) before citing any `runtimeConfig`, `useState`, `$fetch`/`event.$fetch`/`useRequestFetch`/`useRequestHeaders`, payload/`devalue`, `routeRules`, or `useResponseHeader` behavior as fact. Both are Nuxt's own documentation site content mirrored into Context7 — treat matches from either as `documentation-based`. - Confirm which Nuxt major the target repo uses (`package.json`'s `nuxt` dependency) before assuming version-specific defaults; the APIs this skill covers are stable across 3/4, but state which major was confirmed when citing a claim. - The third-party `nuxt-security` module's exact default header set and configuration surface is **not** covered by Nuxt's own Context7-indexed docs — never state a specific default for it as `documentation-based`. Confirm its presence/config by reading the repo's `nuxt.config.ts` directly, and label any claim about its behavior `inference` unless corroborated by the module's own documentation (not currently in scope for this skill's Context7 grounding). - Do not invent API names. If Context7 does not confirm an API or default, say so explicitly and label the claim `inference`. - If Context7 is unavailable, fall back to the `official_docs` URLs in this skill's `metadata.json` and label the claim `documentation-based, unverified against current release`. ## Lean operating rules - Every finding in this skill's five defect classes defaults to HIGH severity (missing-security-headers may be MEDIUM-to-HIGH depending on the app's actual surface — see `references/ssrf-payload-and-response-headers.md`, Part 3). Do not downgrade a structural `runtimeConfig` exposure, cross-request state leak, SSRF path, or payload-XSS trace to informational because it has not been observed exploited yet. - Trace every finding to a concrete file:line and a concrete data-flow path. A finding that says "this might leak the secret" or "this fetch call could be SSRF" without showing the specific config key, the specific module-scope declaration and its reachability, or the specific origin-to-sink trace is a guess, not a finding. - Classify every `runtimeConfig` key by its actual nesting (top-level = private, under `public` = client-exposed) — never by variable name alone. A key named `apiSecret` sitting inside `public` is exposed; a key named `baseUrl` sitting outside `public` is still private and not itself a finding. - Classify every module-scope declaration on two axes before flagging it: mutability/reactivity (only `useState`/`ref`/`reactive`/mutable objects are at risk; immutable constants are not), and reachability from server-rendered code (a declaration no server-rendered path ever touches is not a finding in this scope). - Do not clear a `$fetch`/`ofetch`/`event.$fetch` call in a server route as safe from SSRF just because it "looks like an API call" — trace the destination URL to its origin and confirm either a hardcoded host or an explicit allowlist check before the request fires. - Do not clear a header-forwarding call (`event.$fetch`'s default forwarding, `useRequestHeaders(...)`, or manual header spreading) as safe just because Nuxt documents the mechanism — the mechanism existing is not the same as its use being scoped to only the headers actually needed and only trusted destinations. - Do not approve a `useState`/payload value reaching a `v-html` binding unless a named sanitizer call is visibly present on that exact traced path — a sanitizer existing elsewhere in the codebase does not clear this bar. - Do not report "no security headers" as cleared just because *a* mechanism exists somewhere in the config — confirm the `routeRules` glob (or module config) actually covers the routes in scope before crediting it. - Never execute, build, or run application code, and never send live requests, as part of this review; this is a static-review skill (Read/Grep/Glob only). - Load only the reference needed for the concern in scope. ## References Load these only when needed: - [Review workflow and findings contract](references/workflow-and-output.md) — use for the step-by-step review procedure, the full decision tree across all five defect classes, and the required output shape. - [runtimeConfig exposure and cross-request state pollution](references/runtime-config-and-cross-request-state.md) — load when reviewing `nuxt.config.ts`'s `runtimeConfig` block, `NUXT_PUBLIC_*` env vars, or `useState`/module-scope reactive/mutable declarations reachable from server-rendered code. - [Server-route SSRF, header forwarding, payload XSS, and missing response headers](references/ssrf-payload-and-response-headers.md) — load when reviewing a `server/api`/`server/routes` handler's outbound `$fetch`/`ofetch`/`event.$fetch`/`useRequestFetch` calls, a `useState`/payload value that renders somewhere, or `routeRules`/security-module configuration. - [Acceptance rubric](references/acceptance-rubric.md) — the authoritative list of defects this skill must catch and the false positives it must not raise; consult when unsure whether a pattern is in scope. ## Response minimum Return, at minimum: - the `runtimeConfig`/`routeRules` blocks, module-scope declarations, server routes, and/or template bindings in scope, - ranked findings with file:line evidence, defect category (`runtimeconfig-exposure` / `cross-request-state-pollution` / `ssrf` / `credential-forwarding` / `payload-xss` / `missing-security-headers`), the concrete data-flow trace, and a fix sketch matching Nuxt's documented pattern, - for every `useState`/payload → render-sink finding, an explicit statement of whether a sanitizer call is present on the traced path — never approve on the assumption one exists elsewhere, - evidence level per finding (`repo evidence`, `documentation-based`, or `inference`), with structural risk findings explicitly labeled as structural risk, not as confirmed-exploited, - verdict (approve / approve-with-notes / block), - open questions or scope the review could not cover (e.g., "confirming actual cross-request leakage requires concurrent-request load testing, not static review").
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.