Vue SSR / Nuxt audit

Vue SSR / Nuxt audit

TL;DR: Vue SSR (typically via Nuxt 3) generates HTML server-side, hydrates client-side. Audit surface: v-html reaching server-rendered content with user data, Nuxt server routes / Nitro endpoints exposed without auth, useState / useFetch payload leaks, runtime config exposure, <NuxtLink external> open-redirect, and the Nitro request body parser quirks. Bug families are similar to React SSR (react-ssr-hydration-bugs) but the API surface differs.

What it is

Nuxt 3 (current major) runs on Nitro server engine, deployable to Node, Vercel, Cloudflare, Netlify, AWS, etc. Architecture:

  • pages/*.vue — page components with <script setup> that runs on both server and client.
  • server/api/*.ts — server-only routes.
  • server/routes/*.ts — custom server routes.
  • server/middleware/*.ts — runs on every request.
  • nuxt.config.ts — runtime config, modules, build settings.

Bug patterns

1. v-html with server data

1
<div v-html="post.body" />
  • Vue does NOT sanitise v-html. Identical risk to React dangerouslySetInnerHTML.
  • SSR renders the raw HTML on server → XSS in initial page + post-hydration.
  • Fix: sanitise on write or via DOMPurify / vue-sanitize.

2. Server-only data reaching client via useState / useFetch

1
const { data: user } = await useFetch('/api/user');
  • Returns the full API response, serialised into HTML payload for hydration.
  • If /api/user returns secrets, they’re in the page HTML.
  • Fix: API returns only client-needed fields; trim server-side.

3. Runtime config leak

1
2
3
4
5
6
7
// nuxt.config.ts
runtimeConfig: {
  apiSecret: '',         // server-only
  public: {
    apiBase: '',          // sent to client
  }
}
  • useRuntimeConfig() returns { apiSecret, public: {...} } on server, only { public: {...} } on client.
  • Bug: secret moved to public accidentally → ships to client.

4. Server routes exposed

  • server/api/admin.ts is publicly reachable by default.
  • Middleware authentication must explicitly cover; per-route auth in handler is the usual approach.
  • Audit: every server/api/* and server/routes/* for auth check.

5. Server middleware order

  • server/middleware/*.ts runs on every request, in filename-alphabetical order.
  • auth.ts after 01-logger.ts (filename order) — logger sees unauth requests with PII.

6. Nitro body parser

  • Default body parser handles JSON, form, multipart.
  • Body size limit configurable; default high.
  • Large body DoS unless cap set.
  • Audit: nitro.experimental config + per-route body size.
  • <NuxtLink to="..."> with user-controlled to → open redirect.
  • <NuxtLink external> for external; if to user-controlled, attacker URL allowed.
  • Fix: validate URLs in template; use allowlist for external links.

8. SSR cache

  • Nitro has built-in route caching (defineCachedHandler).
  • If cache key doesn’t include user identity, cross-user data leak.

9. Server-only composables called in client context

  • useRequestHeaders / useRequestEvent are server-only.
  • If called in <script setup> that runs both, the client version returns nothing — but the server version returns headers including cookies. Composable correctness matters.

10. SQL via Nuxt-DB modules

  • Several community modules wrap raw DB. Audit per-module for parameterisation.
  • Direct mysql2/pg usage same shape as nodejs-code-auditing.

11. Module CVE history

  • Nuxt modules ecosystem is large; some have RCE / SSRF CVEs.
  • npm audit + Snyk catch most.

12. Static-generation leak

  • nuxt generate produces static HTML. If pages SSR’d with user-specific data and then statically generated, the static file leaks.
  • Common in misuse: developer uses useFetch with user cookie on page meant to be static.

Audit grep

1
2
3
4
5
6
7
rg -n 'v-html' pages/ components/
rg -n 'useFetch\(|useAsyncData\(' pages/ components/
rg -n 'runtimeConfig|useRuntimeConfig' src/ nuxt.config.*
rg -n 'server/api|server/routes' --files
rg -n 'defineEventHandler' server/ -A5 | grep -v 'auth'
rg -n 'defineCachedHandler|cachedFunction' server/
rg -n '<NuxtLink' --type=vue

Hardening

  • Move all secrets to non-public runtimeConfig.
  • Wrap every server/api/* handler in auth check OR add a covering middleware first in alphabetical order.
  • DTOs for API responses; never return raw DB rows.
  • Cap body size in nitro config.
  • Cache keys include user identity for any authed data.
  • DOMPurify for v-html consumers.
  • Static generate audit: which pages are truly static?

References