Nuxt Client-Side Path Traversal in Nuxt Island Payload Revival
Description
Nuxt is an open-source web development framework for Vue.js. Prior to 3.19.0 and 4.1.0, A client-side path traversal vulnerability in Nuxt's Island payload revival mechanism allowed attackers to manipulate client-side requests to different endpoints within the same application domain when specific prerendering conditions are met. The vulnerability occurs in the client-side payload revival process (revive-payload.client.ts) where Nuxt Islands are automatically fetched when encountering serialized __nuxt_island objects. During prerendering, if an API endpoint returns user-controlled data containing a crafted __nuxt_island object, he data gets serialized with devalue.stringify and stored in the prerendered page. When a client navigates to the prerendered page, devalue.parse deserializes the payload. The Island reviver attempts to fetch /__nuxt_island/${key}.json where key could contain path traversal sequences. Update to Nuxt 3.19.0+ or 4.1.0+.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A client-side path traversal in Nuxt's Island payload revival mechanism allows attackers to redirect requests to arbitrary endpoints within the same domain when prerendering is used.
Vulnerability
Overview
CVE-2025-59414 is a client-side path traversal vulnerability in Nuxt's Island payload revival mechanism. The bug resides in the client-side file revive-payload.client.ts, where Nuxt Islands are automatically fetched when the client encounters serialized __nuxt_island objects. During prerendering, if an API endpoint returns user-controlled data containing a crafted __nuxt_island object, that data is serialized with devalue.stringify and stored in the prerendered page. When a client navigates to that page, __nuxt_island/${key}.json, the key parameter can contain path traversal sequences like ../../, allowing the attacker to manipulate the client-side request to a different endpoint within the same application domain [1][4].
Exploitation
Conditions
Exploitation requires three specific conditions: the application must use Nuxt's prerendering feature (nitro.prerender), the attacker must be able to control the response content of an API endpoint called during prerendering via useFetch, useAsyncData, or similar composables, and a user must navigate to the prerendered page (not during initial SSR hydration) [4]. The attack scenario involves a malicious API response containing a crafted __nuxt_island object with a path traversal payload in the key field, such as "../../../../internal/service", which would cause the client to fetch /__nuxt_island/../../../../internal/service.json [4].
Impact
The impact is limited due to the highly specific prerequisites. The vulnerability does not directly expose sensitive data or allow server-side exploitation; requests originate from the client, not the server. An attacker could potentially probe internal endpoints or perform actions within the same domain, but no direct data exfiltration is possible [4].
Mitigation
The vulnerability is fixed in Nuxt versions 3.19.0 and 4.1.0. The fix introduces a validation function isValidIslandKey that rejects keys containing path traversal sequences, invalid characters, or missing the required underscore separator [2]. Users should update to the affected versions should update immediately [1][4].
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
nuxtnpm | >= 3.6.0, < 3.19.0 | 3.19.0 |
nuxtnpm | >= 4.0.0, < 4.1.0 | 4.1.0 |
Affected products
2- nuxt/nuxtv5Range: >= 3.6.0 < 3.19.0
Patches
12566d2046bccfix(nuxt): add validation for nuxt island reviver key (#33069)
4 files changed · +93 −2
packages/nuxt/src/app/plugins/revive-payload.server.ts+2 −1 modified@@ -5,6 +5,7 @@ import { defineNuxtPlugin } from '../nuxt' // @ts-expect-error Virtual file. import { componentIslands } from '#build/nuxt.config.mjs' +import { isValidIslandKey } from './utils' const reducers: [string, (data: any) => any][] = [ ['NuxtError', data => isNuxtError(data) && data.toJSON()], @@ -17,7 +18,7 @@ const reducers: [string, (data: any) => any][] = [ ] if (componentIslands) { - reducers.push(['Island', data => data && data?.__nuxt_island]) + reducers.push(['Island', data => data && data?.__nuxt_island && isValidIslandKey(data.__nuxt_island.key) && data.__nuxt_island]) } export default defineNuxtPlugin({
packages/nuxt/src/app/plugins/utils.ts+5 −0 added@@ -0,0 +1,5 @@ +const VALID_ISLAND_KEY_RE = /^[a-z][a-z\d-]*_[a-z\d]+$/i +/* @__PURE__ */ +export function isValidIslandKey (key: string): boolean { + return typeof key === 'string' && VALID_ISLAND_KEY_RE.test(key) && key.length <= 100 +}
packages/nuxt/test/plugin-utils.test.ts+85 −0 added@@ -0,0 +1,85 @@ +import { isValidIslandKey } from '#app/plugins/utils' +import { describe, expect, it } from 'vitest' +import { hash } from 'ohash' +import { randomUUID } from 'node:crypto' + +describe('isValidIslandKey util', () => { + it('should accept valid island keys', () => { + // Valid keys following the componentName_hashId pattern + expect(isValidIslandKey('MyComponent_abc123')).toBe(true) + expect(isValidIslandKey('UserCard_def456')).toBe(true) + expect(isValidIslandKey('NavBar_xyz789')).toBe(true) + expect(isValidIslandKey('A_1')).toBe(true) + expect(isValidIslandKey('Component123_hash456')).toBe(true) + expect(isValidIslandKey('my-component_hash123')).toBe(true) + expect(isValidIslandKey('Component-Name_hash123')).toBe(true) + const sampleHash = hash({ props: randomUUID() }).replace(/[-_]/g, '') + expect(isValidIslandKey('ComponentName_' + sampleHash)).toBe(true) + }) + + it('should reject invalid island keys', () => { + // Empty or null/undefined + expect(isValidIslandKey('')).toBe(false) + expect(isValidIslandKey(null as any)).toBe(false) + expect(isValidIslandKey(undefined as any)).toBe(false) + + // Missing underscore separator + expect(isValidIslandKey('ComponentName')).toBe(false) + expect(isValidIslandKey('hash123')).toBe(false) + + // Invalid characters + expect(isValidIslandKey('Component/Name_hash123')).toBe(false) + expect(isValidIslandKey('Component\\Name_hash123')).toBe(false) + expect(isValidIslandKey('Component..Name_hash123')).toBe(false) + expect(isValidIslandKey('Component Name_hash123')).toBe(false) + expect(isValidIslandKey('Component<script>_hash123')).toBe(false) + expect(isValidIslandKey('Component"_hash123')).toBe(false) + expect(isValidIslandKey('Component\'_hash123')).toBe(false) + + // Starting with invalid characters + expect(isValidIslandKey('123Component_hash123')).toBe(false) + expect(isValidIslandKey('_Component_hash123')).toBe(false) + expect(isValidIslandKey('-Component_hash123')).toBe(false) + + // Path traversal attempts + expect(isValidIslandKey('../Component_hash123')).toBe(false) + expect(isValidIslandKey('../../Component_hash123')).toBe(false) + expect(isValidIslandKey('Component_../hash123')).toBe(false) + expect(isValidIslandKey('Component_../../hash123')).toBe(false) + + // URL/protocol attempts + expect(isValidIslandKey('http://evil.com_hash123')).toBe(false) + expect(isValidIslandKey('file:///etc/passwd_hash123')).toBe(false) + expect(isValidIslandKey('Component_http://evil.com')).toBe(false) + + const longKey = 'A'.repeat(95) + '_' + 'B'.repeat(10) + expect(isValidIslandKey(longKey)).toBe(false) + + expect(isValidIslandKey('Component_Name_hash123')).toBe(false) + expect(isValidIslandKey('Component__hash123')).toBe(false) + }) + + it('should handle edge cases', () => { + // Maximum allowed length (100 chars) + const maxLengthKey = 'A'.repeat(94) + '_' + 'B'.repeat(5) // 100 chars total + expect(isValidIslandKey(maxLengthKey)).toBe(true) + + // Just over maximum length + const overLengthKey = 'A'.repeat(95) + '_' + 'B'.repeat(6) // 102 chars total + expect(isValidIslandKey(overLengthKey)).toBe(false) + + // Minimum valid length + expect(isValidIslandKey('A_1')).toBe(true) + + // Single character component name with long hash + expect(isValidIslandKey('A_' + 'B'.repeat(97))).toBe(true) // 100 chars total + }) + + it('should reject non-string inputs', () => { + expect(isValidIslandKey(123 as any)).toBe(false) + expect(isValidIslandKey({} as any)).toBe(false) + expect(isValidIslandKey([] as any)).toBe(false) + expect(isValidIslandKey(true as any)).toBe(false) + expect(isValidIslandKey(Symbol('test') as any)).toBe(false) + }) +})
test/bundle.test.ts+1 −1 modified@@ -117,7 +117,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM const serverDir = join(pagesRootDir, '.output/server') const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir) - expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"288k"`) + expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"289k"`) const modules = await analyzeSizes(['node_modules/**/*'], serverDir) expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1420k"`)
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-p6jq-8vc4-79f6ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-59414ghsaADVISORY
- github.com/nuxt/nuxt/commit/2566d2046bccb158d98fb13e42ce4b2c33fb2595ghsax_refsource_MISCWEB
- github.com/nuxt/nuxt/security/advisories/GHSA-p6jq-8vc4-79f6ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.