{"id":"GHSA-fx6j-w5w5-h468","summary":"Nuxt: Reflected XSS in `navigateTo()` external redirect","details":"### Summary\n`navigateTo()` with `external: true` generates a server-side HTML redirect body containing a `\u003cmeta http-equiv=\"refresh\"\u003e` tag. The destination URL is only sanitized by replacing `\"` with `%22`, leaving `\u003c`, `\u003e`, `&`, and `'` unencoded. An attacker who can influence the URL passed to `navigateTo(url, { external: true })` can break out of the `content=\"…\"` attribute and inject arbitrary HTML/JavaScript that executes under the application's origin.\n\nThis is a different root cause from CVE-2024-34343 (GHSA-vf6r-87q4-2vjf), which addressed `javascript:` protocol bypass. The issue here is triggered by any valid URL containing `\u003e`.\n\n### Impact\nApplications that pass user-controlled input to `navigateTo(url, { external: true })` — typically via a `?next=` / `?redirect=` query parameter used for post-login or \"return to\" flows — are vulnerable to reflected cross-site scripting. The injected script runs in the context of the application's origin during the server-rendered redirect response, before the meta-refresh fires.\n\n### Details\nIn `packages/nuxt/src/app/composables/router.ts`, the SSR redirect path builds an HTML response body with only `\"` percent-encoded in the destination URL:\n\n```ts\nconst encodedLoc = location.replace(/\"/g, '%22')\nnuxtApp.ssrContext!['~renderResponse'] = {\nstatus: sanitizeStatusCode(options?.redirectCode || 302, 302),\nbody: `\u003c!DOCTYPE html\u003e\u003chtml\u003e\u003chead\u003e\u003cmeta http-equiv=\"refresh\" content=\"0; url=${encodedLoc}\"\u003e\u003c/head\u003e\u003c/html\u003e`,\nheaders: { location: encodeURL(location, isExternalHost) },\n}\n```\n\nThe `Location` header is normalised through `encodeURL()` (which uses the `URL` constructor and correctly percent-encodes attribute-significant characters). The HTML body uses a narrower sanitiser. That mismatch is the root cause.\n\n### Proof of concept\n\nGlobal middleware that forwards a query parameter to `navigateTo`:\n\n```ts\n// middleware/redirect.global.ts\nexport default defineNuxtRouteMiddleware((to) =\u003e {\nconst next = to.query.next as string | undefined\nif (next) {\n return navigateTo(next, { external: true })\n}\n})\n```\n\nRequest:\n\n```\nGET /?next=https://evil.example/x\u003e\u003cimg src=x onerror=alert(document.domain)\u003e\n```\n\nResponse body:\n\n```html\n\u003c!DOCTYPE html\u003e\u003chtml\u003e\u003chead\u003e\u003cmeta http-equiv=\"refresh\" content=\"0; url=https://evil.example/x\u003e\u003cimg src=x onerror=alert(document.domain)\u003e\"\u003e\u003c/head\u003e\u003c/html\u003e\n```\n\nThe `\u003e` after `evil.example/x` terminates the `content=\"…\"` attribute, and the `\u003cimg onerror\u003e` tag executes JavaScript in the application's origin before any redirect\noccurs.\n\n### Patches\nFixed in `nuxt@4.4.6` and `nuxt@3.21.6` by [#35052](https://github.com/nuxt/nuxt/pull/35052). The fix percent-encodes the full set of HTML-attribute-significant characters (`&`, `\"`, `'`, `\u003c`, `\u003e`) before interpolating the URL into the meta-refresh body\n\n### Workarounds\nIf you can't upgrade immediately, validate user-controlled URLs before passing them to `navigateTo(url, { external: true })`. At minimum, normalise through `new URL(input).toString()` and reject inputs containing `\u003c` or `\u003e` (a normalised URL with these characters is malformed and safe to refuse).","aliases":["CVE-2026-45669"],"modified":"2026-07-08T17:45:15.337705816Z","published":"2026-05-19T15:49:25Z","database_specific":{"github_reviewed_at":"2026-05-19T15:49:25Z","nvd_published_at":"2026-06-12T14:16:31Z","cwe_ids":["CWE-83"],"severity":"MODERATE","github_reviewed":true},"references":[{"type":"WEB","url":"https://github.com/nuxt/nuxt/security/advisories/GHSA-fx6j-w5w5-h468"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-45669"},{"type":"WEB","url":"https://github.com/nuxt/nuxt/pull/35052"},{"type":"PACKAGE","url":"https://github.com/nuxt/nuxt"}],"affected":[{"package":{"name":"nuxt","ecosystem":"npm","purl":"pkg:npm/nuxt"},"ranges":[{"type":"SEMVER","events":[{"introduced":"3.4.3"},{"fixed":"3.21.6"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/05/GHSA-fx6j-w5w5-h468/GHSA-fx6j-w5w5-h468.json","last_known_affected_version_range":"\u003c= 3.21.5"}},{"package":{"name":"nuxt","ecosystem":"npm","purl":"pkg:npm/nuxt"},"ranges":[{"type":"SEMVER","events":[{"introduced":"4.0.0-alpha.1"},{"fixed":"4.4.6"}]}],"database_specific":{"last_known_affected_version_range":"\u003c= 4.4.5","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/05/GHSA-fx6j-w5w5-h468/GHSA-fx6j-w5w5-h468.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N"},{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:L/VI:L/VA:N/SC:L/SI:L/SA:N"}]}