SECRET DATA
\n\n```\n\nwith `middleware/auth.ts` blocking unauthenticated access:\n\n```bash\n# Direct page request: blocked by middleware\ncurl -i http://localhost:3000/secret\n# -> 403 / redirect, depending on the middleware\n\n# Island request: middleware did not run before this fix\ncurl -i 'http://localhost:3000/__nuxt_island/page_secret_anyhash'\n# -> 200 OK, body includesSECRET DATA
\n```\n\n### Patches\n\nPatched in `nuxt@4.4.6` and `nuxt@3.21.6` by [#35092](https://github.com/nuxt/nuxt/pull/35092). The Vue Router plugin now runs middleware and redirect handling for `page_*` islands (i.e. islands that originate from `.server.vue` files in `pages/`). The island handler propagates middleware-issued responses (`~renderResponse`), and a new `beforeResolve` guard returns HTTP 400 when the requested `page_Nuxt's route middleware is not enforced when rendering `.server.vue` pages via `/__nuxt_island/page_*`
Description
Summary
When experimental.componentIslands is enabled (default in Nuxt 4), any .server.vue file under pages/ is automatically registered as a server island under the key page_ and exposed via the /__nuxt_island/:name endpoint. Until this fix, requests through that endpoint rendered the page component directly via the SSR renderer without instantiating Vue Router, which meant route middleware declared on the page (including definePageMeta({ middleware })) did not run.
For Nuxt applications that gate a .server.vue *page* behind route middleware as their sole auth check, an unauthenticated attacker could bypass that check by requesting /__nuxt_island/page__ directly and receiving the server-rendered HTML.
Affected configurations
All three conditions must hold for an application to be vulnerable:
experimental.componentIslandsis enabled (the default in Nuxt 4; opt-in in Nuxt 3).- The application defines one or more
.server.vuefiles underpages/, registering them as routed pages. - Authentication / authorization for at least one such page is enforced solely via route middleware (
middleware/*.tsreferenced fromdefinePageMeta), without a server-side check inside the page or its data layer.
Applications that enforce auth inside the island's own data layer (server-only API routes, useRequestEvent + manual session checks, etc.) were not affected. The general "route middleware does not run for non-page island *components*" behaviour is documented and unchanged; this advisory concerns the .server.vue *page* case specifically, where running middleware is the user's clear expectation.
Details
- Build (
packages/nuxt/src/components/templates.ts):.server.vuepages are registered as island components withpage_prefix, making them addressable through/__nuxt_island/page__. - Runtime (
packages/nitro-server/src/runtime/handlers/island.ts): the handler resolves the requested island component and renders it viarenderer.renderToString(ssrContext). The Vue Router plugin previously short-circuited middleware execution wheneverssrContext.islandContextwas set. - The two paths interact so that route middleware declared on the source page never runs.
Proof of concept
Given a page app/pages/secret.server.vue:
SECRET DATA
with middleware/auth.ts blocking unauthenticated access:
# Direct page request: blocked by middleware
curl -i http://localhost:3000/secret
# -> 403 / redirect, depending on the middleware
# Island request: middleware did not run before this fix
curl -i 'http://localhost:3000/__nuxt_island/page_secret_anyhash'
# -> 200 OK, body includes SECRET DATA
Patches
Patched in nuxt@4.4.6 and nuxt@3.21.6 by #35092. The Vue Router plugin now runs middleware and redirect handling for page_* islands (i.e. islands that originate from .server.vue files in pages/). The island handler propagates middleware-issued responses (~renderResponse), and a new beforeResolve guard returns HTTP 400 when the requested page_ does not match the route component the URL resolves to.
Non-page island components are unaffected - they continue to render without route middleware, by design.
Workarounds
If you cannot upgrade immediately:
- Enforce authentication inside the
.server.vuepage itself, not via route middleware. Read the session fromuseRequestEvent()andthrow createError({ statusCode: 401 })(or redirect) before returning data. This is the recommended pattern for islands regardless of this advisory. - Disable
experimental.componentIslandsif your app does not use the feature. - If your app must keep route-middleware-only auth, gate the
/__nuxt_island/page_*URL prefix at your reverse proxy or in a server middleware.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Server island endpoint bypasses route middleware on `.server.vue` pages, allowing unauthenticated access to auth-gated content in Nuxt 3/4.
Vulnerability
Nuxt versions prior to the fix (in Nuxt 3.x with experimental.componentIslands enabled, and default in Nuxt 4) automatically register any .server.vue file under pages/ as a server island with a key page_ and expose it via the /__nuxt_island/:name endpoint. When a request is made to this endpoint, the SSR renderer renders the page component directly without instantiating Vue Router, so route middleware declared via definePageMeta({ middleware }) does not execute. This affects applications where authentication or authorization for a .server.vue page relies solely on such middleware [1], [2].
Exploitation
An unauthenticated attacker only needs network access to the Nuxt application endpoint. By crafting a request to /__nuxt_island/page__ (e.g., /__nuxt_island/page_admin_abc123), they can directly retrieve the server-rendered HTML of the restricted page without triggering any route middleware. No authentication, user interaction, or special privileges are required [1], [2].
Impact
Successful exploitation leads to unauthorized disclosure of the server-rendered HTML of pages protected only by route middleware. This can expose sensitive information, such as admin dashboards, private data, or internal application state, that the attacker would otherwise be unable to access. The compromise is limited to information disclosure; however, depending on the page content, it may enable further attacks [1], [2].
Mitigation
The vulnerability is fixed in the latest Nuxt versions following pull request #35092 [4]. Users should update to the patched version that ensures route middleware runs when rendering .server.vue pages via the island endpoint. As a workaround, applications can enforce authentication inside the island's own data layer (e.g., server-only API routes, manual session checks in useRequestEvent) rather than relying solely on route middleware. No evidence of active exploitation or KEV listing is mentioned in the references [1], [2], [4].
AI Insight generated on May 29, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
1e6c9ab5f4835fix(nuxt): run middleware for page islands (#35092)
8 files changed · +140 −20
packages/nitro-server/src/runtime/handlers/island.ts+31 −2 modified@@ -39,19 +39,35 @@ const handler: ReturnType<typeof defineEventHandler> = defineEventHandler(async const renderer = await getSSRRenderer() const renderResult = await renderer.renderToString(ssrContext).catch(async (err) => { + if (ssrContext['~renderResponse'] && (err as Error)?.message === 'skipping render') { + return {} as Awaited<ReturnType<typeof renderer.renderToString>> + } await ssrContext.nuxt?.hooks.callHook('app:error', err) throw err }) + // Fire `app:rendered` before checking `~renderResponse` (matches `renderer.ts`), so + // anything hooking into it, like `useCookie`, will still work on redirect/reject. + await ssrContext.nuxt?.hooks.callHook('app:rendered', { ssrContext, renderResult }) + + if (ssrContext['~renderResponse']) { + const response = ssrContext['~renderResponse'] + if (response.status && response.status >= 400) { + throw new HTTPError({ + status: response.status, + statusText: response.statusText, + }) + } + return returnIslandResponse(event, response) + } + // Handle errors if (ssrContext.payload?.error) { throw ssrContext.payload.error } const inlinedStyles = await renderInlineStyles(ssrContext.modules ?? []) - await ssrContext.nuxt?.hooks.callHook('app:rendered', { ssrContext, renderResult }) - if (inlinedStyles.length) { ssrContext.head.push({ style: inlinedStyles }) } @@ -109,6 +125,19 @@ const handler: ReturnType<typeof defineEventHandler> = defineEventHandler(async export default handler +function returnIslandResponse (event: H3Event, response: Partial<RenderResponse>) { + for (const header in response.headers || {}) { + event.res.headers.set(header, response.headers![header]!) + } + if (response.status) { + event.res.status = response.status + } + if (response.statusText) { + event.res.statusText = response.statusText + } + return response.body +} + const ISLAND_PATH_PREFIX = '/__nuxt_island/' const VALID_COMPONENT_NAME_RE = /^[a-z][\w.-]*$/i
packages/nuxt/src/components/runtime/server-component.ts+8 −2 modified@@ -37,8 +37,8 @@ export const createServerComponent = (name: string) => { } /* @__NO_SIDE_EFFECTS__ */ -export const createIslandPage = (name: string) => { - return defineComponent({ +export const createIslandPage = (name: string, islandKey?: string) => { + const component = defineComponent({ name, inheritAttrs: false, props: { lazy: Boolean }, @@ -72,4 +72,10 @@ export const createIslandPage = (name: string) => { } }, }) + + // we use this to validate that a server page is rendering the correct url + if (import.meta.server && islandKey) { + (component as any).__nuxt_island = islandKey + } + return component }
packages/nuxt/src/components/templates.ts+12 −3 modified@@ -1,5 +1,6 @@ import { isAbsolute, join, relative, resolve } from 'pathe' import { genDynamicImport, genDynamicTypeImport, genObjectKey } from 'knitwork' +import { hash } from 'ohash' import { distDir } from '../dirs.ts' import type { NuxtApp, NuxtPluginTemplate, NuxtTemplate } from 'nuxt/schema' @@ -79,7 +80,7 @@ export const componentsIslandsTemplate: NuxtTemplate = { filename: 'components.islands.mjs', getContents ({ app, nuxt }) { if (!nuxt.options.experimental.componentIslands) { - return 'export const islandComponents = {}' + return 'export const islandComponents = {}\nexport const pageIslandRoutes = {}' } const components = app.components @@ -90,9 +91,14 @@ export const componentsIslandsTemplate: NuxtTemplate = { (component.mode === 'server' && !components.some(c => c.pascalName === component.pascalName && c.mode === 'client')), ) - const pageExports = pages?.filter(p => (p.mode === 'server' && p.file && p.name)).map((p) => { + const serverPages = pages?.filter(p => (p.mode === 'server' && p.file && p.name)) || [] + const pageExports = serverPages.map((p) => { return `"page_${p.name}": defineAsyncComponent(${genDynamicImport(p.file!)}.then(c => c.default || c))` - }) || [] + }) + // map each `page_<name>` to a stable, opaque marker derived from the page's file path. + const pageIslandRoutes = serverPages.map((p) => { + return ` "page_${p.name}": ${JSON.stringify(hash(relative(nuxt.options.rootDir, p.file!)))}` + }) return [ 'import { defineAsyncComponent } from \'vue\'', @@ -105,6 +111,9 @@ export const componentsIslandsTemplate: NuxtTemplate = { }, ).concat(pageExports).join(',\n'), '}', + 'export const pageIslandRoutes = import.meta.client ? {} : {', + pageIslandRoutes.join(',\n'), + '}', ].join('\n') }, }
packages/nuxt/src/pages/runtime/plugins/router.ts+24 −4 modified@@ -18,6 +18,8 @@ import _routes, { handleHotUpdate } from '#build/routes' import routerOptions, { hashMode } from '#build/router.options.mjs' // @ts-expect-error virtual file import { globalMiddleware, namedMiddleware } from '#build/middleware' +// @ts-expect-error virtual file +import { pageIslandRoutes } from '#build/components.islands.mjs' // https://github.com/vuejs/router/blob/4a0cc8b9c1e642cdf47cc007fa5bbebde70afc66/packages/router/src/history/html5.ts#L37 function createCurrentLocation ( @@ -140,7 +142,9 @@ const plugin: Plugin<{ router: Router }> = defineNuxtPlugin({ } const error = useError() - if (import.meta.client || !nuxtApp.ssrContext?.islandContext) { + // we only skip redirect handlers for component islands, not page islands + const isServerPage = import.meta.server && nuxtApp.ssrContext?.islandContext?.name?.startsWith('page_') + if (import.meta.client || !nuxtApp.ssrContext?.islandContext || isServerPage) { router.afterEach(async (to, _from, failure) => { delete nuxtApp._processingMiddleware @@ -188,8 +192,8 @@ const plugin: Plugin<{ router: Router }> = defineNuxtPlugin({ syncCurrentRoute() - if (import.meta.server && nuxtApp.ssrContext?.islandContext) { - // We're in an island context, and don't need to handle middleware or redirections + if (import.meta.server && nuxtApp.ssrContext?.islandContext && !isServerPage) { + // we don't need to handle middleware or redirections for non-page islands return { provide: { router } } } @@ -202,7 +206,7 @@ const plugin: Plugin<{ router: Router }> = defineNuxtPlugin({ } nuxtApp._processingMiddleware = true - if (import.meta.client || !nuxtApp.ssrContext?.islandContext) { + if (import.meta.client || !nuxtApp.ssrContext?.islandContext || isServerPage) { type MiddlewareDef = string | RouteMiddleware const middlewareEntries = new Set<MiddlewareDef>([...globalMiddleware, ...nuxtApp._middleware.global]) for (const component of to.matched) { @@ -272,6 +276,22 @@ const plugin: Plugin<{ router: Router }> = defineNuxtPlugin({ } }) + if (isServerPage) { + // validate that a server page is rendering the correct url + router.beforeResolve((to) => { + const expected = pageIslandRoutes[nuxtApp.ssrContext!.islandContext!.name] + const actual = to.matched.find(m => (m.components?.default as any)?.__nuxt_island) + ?.components?.default as any + if (!expected || expected !== actual?.__nuxt_island) { + nuxtApp.ssrContext!['~renderResponse'] = { + status: 400, + statusText: 'Invalid island request path', + } + return false + } + }) + } + router.onError(async () => { delete nuxtApp._processingMiddleware await nuxtApp.callHook('page:loading:end')
packages/nuxt/src/pages/utils.ts+15 −9 modified@@ -1,7 +1,7 @@ import { runInNewContext } from 'node:vm' import fs from 'node:fs' -import { normalize } from 'pathe' +import { normalize, relative } from 'pathe' import { joinURL } from 'ufo' import { getLayerDirectories, resolveFiles, resolvePath, useNuxt } from '@nuxt/kit' import { genArrayFromRaw, genDynamicImport, genImport, genSafeVariableName } from 'knitwork' @@ -342,23 +342,23 @@ interface NormalizeRoutesOptions { clientComponentRuntime: string } -function normalizeComponent (page: NuxtPage, pageImport: string, routeName: string | undefined): string { +function normalizeComponent (page: NuxtPage, pageImport: string, routeName: string | undefined, islandKey: string | undefined): string { if (page.mode === 'server') { - return `() => createIslandPage(${routeName})` + return `() => createIslandPage(${routeName}, import.meta.server ? ${islandKey} : undefined)` } if (page.mode === 'client') { return `() => createClientPage(${pageImport})` } return pageImport } -function normalizeComponentWithName (page: NuxtPage, isSyncImport: boolean | undefined, pageImportName: string, pageImport: string, routeName: string | undefined, metaRouteName: string): string { +function normalizeComponentWithName (page: NuxtPage, isSyncImport: boolean | undefined, pageImportName: string, pageImport: string, routeName: string | undefined, metaRouteName: string, islandKey: string | undefined): string { if (isSyncImport) { return `Object.assign(${pageImportName}, { __name: ${metaRouteName} })` } // Server components already receive the name via createIslandPage(name) if (page.mode === 'server') { - return `() => createIslandPage(${routeName})` + return `() => createIslandPage(${routeName}, import.meta.server ? ${islandKey} : undefined)` } // Client components return a processed component (not a module with .default) if (page.mode === 'client') { @@ -381,6 +381,7 @@ export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> = metaFiltered[key] = page.meta![key] } } + const skipAlias = toArray(page.alias).every(val => !val) const route: NormalizedRoute = { @@ -420,9 +421,14 @@ export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> = const pageImport = isSyncImport ? pageImportName : genDynamicImport(file) const metaRouteName = `${metaImportName}?.name ?? ${route.name}` + // we use this to validate that a server page is rendering the correct url + const islandKey = page.mode === 'server' && page.file + ? JSON.stringify(hash(relative(nuxt.options.rootDir, page.file))) + : undefined + const component = nuxt.options.experimental.normalizePageNames - ? normalizeComponentWithName(page, isSyncImport, pageImportName, pageImport, route.name, metaRouteName) - : normalizeComponent(page, pageImport, route.name) + ? normalizeComponentWithName(page, isSyncImport, pageImportName, pageImport, route.name, metaRouteName, islandKey) + : normalizeComponent(page, pageImport, route.name, islandKey) const metaRoute: NormalizedRoute = { name: metaRouteName, @@ -437,9 +443,9 @@ export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> = if (page.mode === 'server') { metaImports.add(` let _createIslandPage -async function createIslandPage (name) { +async function createIslandPage (name, islandKey) { _createIslandPage ||= await import(${JSON.stringify(options?.serverComponentRuntime)}).then(r => r.createIslandPage) - return _createIslandPage(name) + return _createIslandPage(name, islandKey) };`) } else if (page.mode === 'client') { metaImports.add(`
test/fixtures/server-components/app/middleware/island-auth.ts+4 −0 added@@ -0,0 +1,4 @@ +export default defineNuxtRouteMiddleware(() => { + useCookie('island-auth-marker').value = 'set-from-island-middleware' + return navigateTo('/login', { redirectCode: 302 }) +})
test/fixtures/server-components/app/pages/gated-server-page.server.vue+11 −0 added@@ -0,0 +1,11 @@ +<template> + <div id="gated-server-page"> + SUPER-SECRET-PAGE-ISLAND-BODY + </div> +</template> + +<script setup lang="ts"> +definePageMeta({ + middleware: 'island-auth', +}) +</script>
test/server-components.test.ts+35 −0 modified@@ -502,6 +502,41 @@ describe('hash binding', () => { }) }) +describe('page-island middleware', () => { + it('runs page middleware and honours redirects for `page_*` islands', async () => { + const res = await fetch(islandURL('page_gated-server-page', { + context: { url: '/gated-server-page' }, + }), { redirect: 'manual' }) + // page middleware calls `navigateTo('/login', { redirectCode: 302 })` + expect(res.status).toBe(302) + expect(res.headers.get('location')).toContain('/login') + const body = await res.text() + expect(body).not.toContain('SUPER-SECRET-PAGE-ISLAND-BODY') + // this asserts the island handler fires `app:rendered` even when middleware short-circuits response + expect(res.headers.get('set-cookie')).toContain('island-auth-marker=set-from-island-middleware') + }) + + it('still renders unguarded `page_*` islands', async () => { + const res = await fetch(islandURL('page_server-page', { + context: { url: '/server-page' }, + })) + expect(res.status).toBe(200) + const body = await res.json() as NuxtIslandResponse + expect(body.html).toContain('Hello this is a server page') + }) + + it('rejects a `page_*` island whose url routes to a different page', async () => { + // Forging `page_gated-server-page` with `url=/server-page` would render the gated + // page's HTML while running the (empty) middleware for the unguarded page. + const res = await fetch(islandURL('page_gated-server-page', { + context: { url: '/server-page' }, + }), { redirect: 'manual' }) + expect(res.status).toBe(400) + const body = await res.text() + expect(body).not.toContain('SUPER-SECRET-PAGE-ISLAND-BODY') + }) +}) + describe.skipIf(isDev || isWebpack)('regressions', () => { // https://github.com/nuxt/nuxt/issues/26527 it.fails('renders <Counter nuxt-client /> when nested two levels deep in server components', async () => {
Vulnerability mechanics
Root cause
"The island SSR handler renders `.server.vue` pages without instantiating Vue Router, so route middleware declared via `definePageMeta({ middleware })` never executes."
Attack vector
An attacker sends a GET request to `/__nuxt_island/page_<routeName>_<anyhash>` — the endpoint that serves server-island components [ref_id=1]. Because the island handler renders the component via `renderer.renderToString(ssrContext)` without instantiating Vue Router, any route middleware (e.g. an auth guard) declared on the source `.server.vue` page is skipped [ref_id=1]. The attacker receives the full server-rendered HTML of the protected page, bypassing the intended access control. The only preconditions are that `experimental.componentIslands` is enabled, a `.server.vue` page exists under `pages/`, and that page relies solely on route middleware for authentication [ref_id=1].
Affected code
Build-time registration in `packages/nuxt/src/components/templates.ts` assigns `.server.vue` pages island component keys with a `page_` prefix [ref_id=1]. Runtime handling in `packages/nitro-server/src/runtime/handlers/island.ts` resolves the island and renders it via `renderer.renderToString(ssrContext)` [ref_id=1]. The Vue Router plugin previously short-circuited middleware execution whenever `ssrContext.islandContext` was set, so route middleware never ran for these `page_*` islands [ref_id=1].
What the fix does
The patch modifies the Vue Router plugin so that route middleware and redirect handling now execute for `page_*` islands — those originating from `.server.vue` files in `pages/` [ref_id=1]. The island handler propagates middleware-issued responses (`~renderResponse`), and a new `beforeResolve` guard returns HTTP 400 when the requested `page_<name>` does not match the route component the URL resolves to [ref_id=1]. Non-page island components remain unaffected and continue to render without route middleware, which is by design [ref_id=1].
Preconditions
- configexperimental.componentIslands must be enabled (default in Nuxt 4, opt-in in Nuxt 3)
- configAt least one .server.vue file must exist under pages/
- configAuthentication for that page must be enforced solely via route middleware (middleware/*.ts referenced from definePageMeta), without additional server-side checks inside the page or its data layer
Reproduction
Create a page `app/pages/secret.server.vue` with `<script setup lang="ts"> definePageMeta({ middleware: 'auth' }) </script>` and a middleware that blocks unauthenticated users. Request the direct page URL (`curl http://localhost:3000/secret`) — the middleware blocks the request. Then request the island endpoint (`curl 'http://localhost:3000/__nuxt_island/page_secret_anyhash'`) — before the fix this returns 200 OK with the protected content [ref_id=1].
Generated on May 29, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.