Claude Cursor GitHub Copilot Skill

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

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download vincentchuwaichow-vanguard-frontier-agentic-skills_frontend_nuxt-fullstack-security-review-febe32a.zip · 20 KB
Part of vincentchuwaichow/vanguard-frontier-agentic — 293 skills

Install

skills CLI npx skills add https://github.com/VincentChuWaiChow/vanguard-frontier-agentic/tree/master/skills/frontend/nuxt-fullstack-security-review
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install vincentchuwaichow-vanguard-frontier-agentic@llmmart
Git 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.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:

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").
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.

No comments yet.

Reviews (0)

No reviews yet.

Related