Skip to content

Nuxt route-rule and SSR payload-cache authority boundaries

Two reviewed Nuxt advisories expose the same durable testing rule: authorization must be evaluated on the exact normalized route and for the current request, not inherited from a mismatched route-rule key or a prior user's shared payload-cache entry.

Primary sources:

  • shared SSR payload cache disclosure: GHSA-wm8w-6qjm-cv43, affecting Nuxt 4.4.0 through 4.5.0 and fixed in 4.5.1; and
  • mixed-case route-rule bypass: GHSA-hxvh-4h3w-prp9, affecting Nuxt 4.4.7 through 4.5.0 and 3.21.7 through 3.21.9, fixed in 4.5.1 and 3.21.10 respectively.

Two-user synthetic fixtures only

Use a local disposable Nuxt application, two fake users or tenants, random non-secret markers, and patched data-return sinks. Never request production payloads, collect cookies or bearer tokens, test another customer's route, or seed real profile, billing, token, or tenant data into SSR state.

1. Inventory the full route-to-payload surface

For each page protected by middleware or a page guard, record:

  • source filename and configured route-rule key;
  • router case-sensitivity setting;
  • requested raw path, normalized path, and matched page;
  • cache, swr, or isr route-rule state;
  • whether runtime payload extraction exposes /<page>/_payload.json;
  • middleware and page-guard decisions for the HTML request and payload request separately;
  • payload cache namespace and effective key dimensions; and
  • the identity or tenant that generated the cached value.

Do not assume the HTML response and extracted payload use the same authorization or cache path. Test them as distinct resources.

2. Test symmetric route normalization

The route-rule regression lowercased lookup paths while leaving mixed-case compiled keys unchanged. Vue Router could still resolve the page case-insensitively, but Nuxt could omit the rule and its appMiddleware gate.

Create synthetic pages and rules covering lowercase, PascalCase, camelCase, wildcard, and nested route keys. Exercise exact-case and case-variant requests while recording both router and route-rule matcher output:

Route-rule key Request variants Required invariant
/admin /admin, /Admin, /ADMIN either all variants resolve and receive the same rule, or disallowed variants do not route
/Admin/dashboard exact, lower, upper, mixed key and lookup use the same normalization policy
/Dashboard/** nested exact and case variants wildcard expansion cannot drop appMiddleware
page-derived mixed-case key direct navigation and payload URL derived rule remains attached to the page selected by the router

Patch the protected handler to return only AUTH-CANARY-<uuid> after recording the middleware decision. A bounded positive is router resolves the protected page -> route-rule matcher omits the mixed-case key -> auth middleware does not run -> no-op protected handler is reached. Do not expose account data or perform a privileged action.

Include router.options.sensitive: true as a control. When case-sensitive routing is intentional, both key compilation and lookup must preserve case and nonmatching variants must fail at routing rather than silently losing policy.

3. Test cache identity and authorization order

For the payload-cache issue, create one cached protected page whose useAsyncData or useFetch result contains only a random marker derived from the current synthetic user. Use user A to warm the page, then request both the HTML route and /<page>/_payload.json as:

  1. user A;
  2. unauthenticated client;
  3. user B in the same tenant;
  4. user B in a different tenant; and
  5. user A after logout or permission revocation.

Capture cookie/header presence as booleans or hashes, never raw credentials. Record route middleware and page-guard invocation counts, cache lookup timing, cache key, hit/miss, marker origin, response status, and marker hash.

A bounded positive is user A warms protected cached route -> later payload request returns A's marker from a path-only runtime cache before current-request middleware or guards run. The HTML response remaining protected is an important differential, not a negative result.

Repeat across cache, swr, and isr, and include:

  • uncached route control;
  • public cached route control containing only globally public data;
  • route with explicit cache.varies dimensions;
  • payload request with no prior HTML navigation;
  • cold and warm server processes; and
  • affected-versus-fixed Nuxt versions.

The secure outcome is current-request authorization before payload return. For private state, either avoid shared runtime payload caching or bind every cache entry to all identity and tenant dimensions that affect the result.

4. Chain the two boundaries without retrieving data

A useful regression fixture combines a mixed-case protected route with payload extraction:

  1. configure a mixed-case page and appMiddleware rule;
  2. warm its cache with user A's random marker;
  3. request exact-case and case-variant HTML and payload paths as no-user and user B;
  4. record router match, rule match, middleware execution, cache lookup, and response-marker hash; and
  5. deny any serializer output other than the known marker.

This distinguishes two independent failures:

  • policy attachment failure: the router selects a page but normalization causes its route rule to disappear; and
  • policy replay failure: a cached payload is returned without re-running authorization for the current request.

Do not collapse them into a generic “auth bypass.” Report which transition failed and whether the proof reached a protected handler, a foreign marker, or both.

Evidence template

Nuxt version and deployment mode:
Router case-sensitivity:
Configured route-rule key:
Raw / normalized / matched path:
HTML or _payload.json request:
cache / swr / isr configuration:
Current synthetic principal and tenant:
Route middleware / page guard invocation:
Cache namespace, redacted key, hit or miss:
Marker creator and returned marker hash:
Affected-versus-fixed result:
Strongest bounded claim:
Excluded data-access or action claims:

Keep response bodies to random markers. A cache hit, route match, or skipped hook alone is not proof of cross-user disclosure; bind the current principal, marker creator, cache decision, and returned marker in one trace.

August 5 follow-up: server-island component and DevTools RPC authority

Three later reviewed records add two distinct Nuxt control planes:

Advisory Boundary Affected / fixed range
GHSA-9473-5f9j-94wq request-supplied server-island props reach Vue dynamic-component resolution and, when vue.runtimeCompiler: true, an attacker-supplied template can reach the Nitro server-side compiler Nuxt 3.4.0 through 3.21.9 and 4.0.0 through 4.5.0; fixed in 3.21.10 and 4.5.1
GHSA-48hr-524c-v5w3 a plain string in a forwarded island prop can instantiate a globally registered component or native element without requiring the runtime compiler Nuxt 3.1.0 through 3.21.9 and 4.0.0 through 4.5.0; fixed in 3.21.10 and 4.5.1
GHSA-279x-mwfv-vcqv an unauthenticated Vite HMR RPC client can change the persisted editor command and then reach openInEditor() on a developer host @nuxt/devtools < 3.3.1; fixed in 3.3.1

Instrument compilers, constructors, and process launchers

Use a disposable Nuxt project, harmless test components, inert template and element markers, a patched Vue compiler/component resolver, and a denied process-spawn recorder. Never compile attacker code, launch an editor or command, expose a developer's real HMR listener, inspect source files, or use a component whose setup performs network, filesystem, credential, or database operations.

Server-island prop-to-component matrix

Inventory every server island that forwards caller-controlled values into <component :is>, resolveDynamicComponent(), h(), or a polymorphic as/asChild prop. Record whether vue.runtimeCompiler is active in the server and client bundles and enumerate globally registered components by name only; do not invoke unknown components.

Build a synthetic island whose only allowed target is SafeCard. Patch the component resolver and runtime compiler so they log their inputs and return inert placeholders. Exercise:

  • the expected SafeCard identifier;
  • an unknown component string;
  • the name of a harmless globally registered canary component;
  • native-element strings such as an inert div and blocked network-capable element classes; and
  • an object containing a template key, with runtime compilation disabled and enabled.

Capture raw props, schema decision, resolved target kind, compiler invocation count, and placeholder output. The two positives are different:

  • component authority expansion: a caller-controlled string resolves a global component or native element outside the island's explicit allowlist; and
  • compiler authority expansion: a caller-controlled object reaches the server runtime compiler when runtime compilation is enabled.

Do not claim server code execution from component selection alone. For the compiler path, stop at the patched compiler receiving the inert marker; do not run generated render functions. Test affected and fixed releases with the same fixture and include direct, encoded, nested, array, and alternate forwarded-prop shapes so validation cannot be bypassed by moving the value one level deeper.

DevTools WebSocket RPC matrix

Treat Nuxt DevTools as a developer-host control plane, not as an ordinary browser debugging widget:

  1. Start a disposable affected Nuxt development server in a container or VM with no repository credentials and bind HMR only to the lab interface.
  2. Replace launch-editor or the final child-process constructor with a recorder that logs redacted argv and always denies execution.
  3. From an owned second client, connect to the Vite HMR WebSocket using the vite-hmr subprotocol and record whether the nuxt:devtools:rpc channel requires a token or validates Origin before accepting calls.
  4. Invoke only an ordinary read-only RPC baseline, then attempt updateOptions, clearOptions, and openInEditor with an inert command-name marker and a known synthetic file path.
  5. Record RPC identity, origin, auth decision, persisted option transition, method result, and denied spawn argv. Repeat on @nuxt/devtools 3.3.1+ and with remote HMR exposure removed.

A bounded positive is unauthenticated client reaches the DevTools RPC channel -> changes the editor-command option -> openInEditor sends the inert marker to the denied process-launch sink. WebSocket reachability or option mutation alone is not command execution. Keep the exact command-bearing RPC body out of public evidence.

Follow-up evidence fields

Nuxt / @nuxt/devtools version:
Development or production mode:
Server-island component and forwarded prop:
vue.runtimeCompiler server/client state:
Raw prop and schema/allowlist decision:
Resolved component or native-element kind:
Runtime-compiler recorder invocation:
HMR bind address and WebSocket Origin:
RPC authentication / origin decision:
Persisted editor-option transition:
Denied process argv recorder:
Affected-versus-fixed result:
Strongest bounded claim and excluded execution claims: