fix: make SSR auth middleware cache-aware to prevent user leak in __NUXT__ payload (#52)#57
fix: make SSR auth middleware cache-aware to prevent user leak in __NUXT__ payload (#52)#57frederikprijck wants to merge 22 commits into
Conversation
The Nuxt app-level getRouteRules() composable reads a build-time manifest that omits bare Cache-Control header route rules, so the guard never detected the shared-cacheable case and PII still leaked into the payload. Read the resolved rules from h3Event.context._nitro.routeRules instead. Adds an end-to-end test (offline, stateless cookie session) demonstrating the leak on a non-cacheable route and its absence on a shared-cacheable route. (#52)
…ernal Replace the `h3Event.context._nitro.routeRules` private-field access with Nitro's supported `getRouteRules(event)` server util (dynamically imported from `nitropack/runtime`, server-only to keep it out of the client bundle). Same resolved rules, no private API. (#52)
CVE-2026-53721) Nitro's route-rule matcher is case-sensitive, but vue-router matches routes case-insensitively. A mixed-case request (e.g. /Cacheable) therefore renders the /cacheable page while resolving NO route rule — bypassing the shared-cacheable cache guard and writing the authenticated user into SSR HTML that the consumer marked publicly cacheable. Nuxt's own fix (4.4.7 / 3.21.7) lowercases the path only in the app-level and nitro-server matchers; the 'nitropack/runtime' getRouteRules path this middleware uses is not covered, so the bypass still reproduces on patched Nuxt. Harden in our own code: after checking the request path as-is, re-resolve the route rules for the lowercased path (via a synthetic event with a fresh context so getRouteRules recomputes) and skip the write if either is shared-cacheable. Skipping is always fail-safe — the client hydrates the user. - middleware: dual lookup (request path + lowercased path) - unit test: shared-cacheable bypass via casing skips the write - e2e: /Cacheable renders anonymous SSR HTML (leaked before the fix)
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (4)
📝 WalkthroughWalkthroughThe module now detects shared-cacheable SSR requests, omits authenticated users from their Nuxt payloads, and hydrates users client-side through a no-store ChangesCache-aware authentication
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Browser
participant SSRMiddleware
participant Auth0Client
participant ProfileHandler
Browser->>SSRMiddleware: request shared-cacheable page
SSRMiddleware->>SSRMiddleware: detect shared cacheability
SSRMiddleware-->>Browser: anonymous SSR payload
Browser->>ProfileHandler: fetch /auth/profile after suspense
ProfileHandler->>Auth0Client: getUser()
Auth0Client-->>ProfileHandler: user or null
ProfileHandler-->>Browser: profile response
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install timed out. The project may have too many dependencies for the sandbox. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…ort.meta.server (#52) The cache guard's `import('nitropack/runtime')` was pulled into the client bundle, breaking `nuxt build` (`Could not load .nuxt/nitro.client.mjs`). `nitropack/runtime` resolves to server-only Nitro virtual modules that cannot load in the client. The `importMetaServer` helper guard does not let the bundler eliminate the import: it is an opaque re-export Vite cannot statically evaluate. Only a literal `import.meta.server` guard folds to `false` in the client build so Rollup tree-shakes the whole block (and the dynamic import) away. But Nuxt constant-folds `import.meta.*` at transform time even under Vitest's node environment, so a literal guard makes the middleware body unreachable in unit tests — which is exactly why the mockable helper existed. To keep fast unit coverage of the branching cache-guard logic (including the CVE-2026-53721 lowercase re-check), it is extracted into a pure, dependency-injected `isRequestSharedCacheable` with its own unit test. The middleware becomes a thin `import.meta.server` shell, covered by the e2e caching fixture. - Add `isRequestSharedCacheable(getRouteRules, event)` + unit spec. - Gate middleware on literal `import.meta.server`; delegate to the pure function. - Remove `auth.server.spec.ts` (logic now covered by the pure-fn spec + e2e). Verified: package + both example apps build, 66 unit tests pass, lint clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
🧹 Nitpick comments (2)
packages/auth0-nuxt/src/runtime/middleware/auth.server.ts (2)
34-38: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winCache the dynamic import to avoid overhead on every request.
While
import()is cached by the module system, calling it still creates a Promise and queues microtasks on every SSR request. For middleware running on every navigation, it is more efficient to cache the resolved function in a module-scoped variable after the first load.⚡ Proposed fix
+let getNitroRouteRules: typeof import('nitropack/runtime').getRouteRules; + export default defineNuxtRouteMiddleware(async () => { if (import.meta.server) { const app = useNuxtApp(); const h3Event = app.ssrContext!.event; - const { getRouteRules } = await import('nitropack/runtime'); + if (!getNitroRouteRules) { + const nitroRuntime = await import('nitropack/runtime'); + getNitroRouteRules = nitroRuntime.getRouteRules; + } - if (isRequestSharedCacheable(getRouteRules, h3Event)) { + if (isRequestSharedCacheable(getNitroRouteRules, h3Event)) {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/auth0-nuxt/src/runtime/middleware/auth.server.ts` around lines 34 - 38, Cache the resolved getRouteRules function at module scope instead of awaiting the Nitro runtime import on every request. Update the middleware flow around isRequestSharedCacheable to lazily initialize and reuse the cached function while preserving the existing fail-closed behavior when route rules are unavailable.
34-38: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winCache the dynamic import to avoid overhead on every request.
While
import()is cached by the module system, calling it still creates a Promise and queues microtasks on every SSR request. For middleware running on every navigation, it is more efficient to cache the resolved function in a module-scoped variable after the first load.⚡ Proposed fix
+let getNitroRouteRules: typeof import('nitropack/runtime').getRouteRules; + export default defineNuxtRouteMiddleware(async () => { if (import.meta.server) { const app = useNuxtApp(); const h3Event = app.ssrContext!.event; - const { getRouteRules } = await import('nitropack/runtime'); + if (!getNitroRouteRules) { + const nitroRuntime = await import('nitropack/runtime'); + getNitroRouteRules = nitroRuntime.getRouteRules; + } - if (isRequestSharedCacheable(getRouteRules, h3Event)) { + if (isRequestSharedCacheable(getNitroRouteRules, h3Event)) {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/auth0-nuxt/src/runtime/middleware/auth.server.ts` around lines 34 - 38, Cache the resolved getRouteRules function at module scope instead of awaiting import('nitropack/runtime') on every request in the middleware flow. Update the initialization path around isRequestSharedCacheable to load it once and reuse the cached function for subsequent requests, while preserving the existing fail-closed behavior when route rules are unavailable.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/auth0-nuxt/src/runtime/middleware/auth.server.ts`:
- Around line 34-38: Cache the resolved getRouteRules function at module scope
instead of awaiting the Nitro runtime import on every request. Update the
middleware flow around isRequestSharedCacheable to lazily initialize and reuse
the cached function while preserving the existing fail-closed behavior when
route rules are unavailable.
- Around line 34-38: Cache the resolved getRouteRules function at module scope
instead of awaiting import('nitropack/runtime') on every request in the
middleware flow. Update the initialization path around isRequestSharedCacheable
to load it once and reuse the cached function for subsequent requests, while
preserving the existing fail-closed behavior when route rules are unavailable.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: a679150b-68e0-41b6-a538-41c9eab99a96
📒 Files selected for processing (21)
packages/auth0-nuxt/EXAMPLES.mdpackages/auth0-nuxt/README.mdpackages/auth0-nuxt/src/module.spec.tspackages/auth0-nuxt/src/module.tspackages/auth0-nuxt/src/runtime/helpers/import-meta.tspackages/auth0-nuxt/src/runtime/middleware/auth.server.tspackages/auth0-nuxt/src/runtime/plugins/auth.client.spec.tspackages/auth0-nuxt/src/runtime/plugins/auth.client.tspackages/auth0-nuxt/src/runtime/server/api/auth/profile.get.spec.tspackages/auth0-nuxt/src/runtime/server/api/auth/profile.get.tspackages/auth0-nuxt/src/runtime/server/utils/is-request-shared-cacheable.spec.tspackages/auth0-nuxt/src/runtime/server/utils/is-request-shared-cacheable.tspackages/auth0-nuxt/src/runtime/server/utils/is-shared-cacheable.spec.tspackages/auth0-nuxt/src/runtime/server/utils/is-shared-cacheable.tspackages/auth0-nuxt/src/types.tspackages/auth0-nuxt/test/caching.test.tspackages/auth0-nuxt/test/fixtures/caching/app.vuepackages/auth0-nuxt/test/fixtures/caching/nuxt.config.tspackages/auth0-nuxt/test/fixtures/caching/package.jsonpackages/auth0-nuxt/test/fixtures/caching/pages/cacheable.vuepackages/auth0-nuxt/test/fixtures/caching/pages/private.vue
…che guard (#52) The cache guard only flagged `public`/`s-maxage`, so a `Cache-Control: max-age=N` route rule still wrote the user into the __NUXT__ payload. RFC 9111's authenticated-response restriction keys on the Authorization header, not cookies, so a path-keyed shared cache can store a cookie-authenticated `max-age` response and serve it across users. Treat a positive `max-age` as shared-cacheable, honor explicit `private`/`no-store` opt-outs, and let `public`/`s-maxage` win over a contradictory `private`.
Problem
The global SSR auth middleware wrote the authenticated user into
useState('auth0_user'), which Nuxt serializes into the__NUXT__payload of the server-rendered HTML. On any shared-cacheable route (CDN /s-maxage/swr/isr), that HTML — with one user's PII baked in — could be stored and served to other visitors.Fix
The middleware now consults the Nitro route rules resolved for the request and skips the SSR user write when the response is shared-cacheable. The user is instead hydrated client-side from a
no-store/auth/profileendpoint, so authenticated UI still works without ever placing PII into a cacheable payload.isSharedCacheable()predicate detectscache/swr/isrrules andpublic/s-maxageCache-Control headers.getRouteRulesserver util (nitropack/runtime).no-store/auth/profileendpoint +.clientplugin re-hydrate the user after load.Closes #52.
Tests
isSharedCacheablepredicate, middleware cache guard (incl. fail-closed + CVE case-variants), profile endpoint, client plugin.cachingfixture asserting the user is server-rendered on ano-storeroute but absent from a shared-cacheable route's raw HTML.Summary by CodeRabbit
/auth/profileroute (and optional customization) for profile fetching withno-storecaching behavior.