Impact
Nuxt looks up routeRules for the current navigation by calling getRouteRules({ path: to.path }) from the page-router plugin and the no-pages router plugin. The compiled routeRules matcher (built on rou3) performs case-sensitive matching, while vue-router is configured with its default sensitive: false and matches paths case-insensitively.
The two routers therefore disagree on which rules apply to a given request path: vue-router still matches the page record for /Admin/dashboard, but the routeRules lookup for the same path returns no match. Any appMiddleware declared via routeRules is never added to the middleware set and never runs, on both SSR and client navigations. The same path skips other path-keyed route rules in the same way (ssr, redirect, appLayout, and the prerender / payload hints used client-side).
For applications using routeRules with appMiddleware as an authorization gate (a documented pattern), an attacker can flip the case of any static segment in a protected URL (for example /Admin/dashboard instead of /admin/dashboard) to render the protected page with the middleware skipped. The server returns the fully server-rendered page including any useFetch / useAsyncData results captured during SSR.
This is an instance of CWE-178 (Improper Handling of Case Sensitivity) leading to CWE-863 (Incorrect Authorization) for apps that treat appMiddleware as an authorization boundary.
Mitigating factors
- Only affects apps that use routeRules.appMiddleware. The more idiomatic definePageMeta({ middleware }) is bound to the matched route record and is unaffected. - Nuxt route middleware is documented as an app-layer concern, not a server-side auth boundary; well-built apps enforce authorization again at the API / data-fetching layer. - Apps that explicitly set router.options.sensitive = true are not affected.
Patches
Fixed in nuxt@4.4.7 (commit 07e39cd6) and backported to nuxt@3.21.7 (commit 3f3e3fa7). The fix normalizes the path used for routeRules lookups so it matches vue-router's default case-insensitive semantics.
Workarounds
Until you can upgrade, you can mitigate by either:
1. Setting router.options.sensitive = true so vue-router matches case-sensitively (this changes route-matching behaviour app-wide). 2. Moving security-critical middleware off routeRules.appMiddleware and onto definePageMeta({ middleware: [...] }) on the protected page components, which is bound to the matched record. 3. Enforcing authorization at the API / data-fetching layer (which you should be doing in any case).
Credit
Reported by Anthropic / Claude through Anthropic's coordinated vulnerability disclosure process. Reference: ANT-2026-9FSEBYMC.
Nuxt 4.0.0 before 4.4.7 and 3.18.0 before 3.21.7, when running the development server (nuxt dev) on Linux, binds the vite-node IPC server to an abstract-namespace Unix socket without permission restrictions, allowing local users to enumerate and connect. Unprivileged co-resident users can exploit the unprotected module request handler to read arbitrary files such as .env and SSH keys through the SSR plugin pipeline. Production builds are unaffected, as the IPC server runs only in development.
Summary
Nuxt compared routeRules keys against the raw request path, without decoding percent-encoding, while the router decodes parameters when it resolves a route. A request whose path is percent-encoded can therefore resolve to a page while failing to match the route rule meant to protect it, so the rule's appMiddleware never runs, including during server rendering.
Impact
For a page served through a dynamic or catch-all route, an attacker can percent-encode a character in the path to skip the rule:
ts export default defineNuxtConfig({ routeRules: { '/users/admin': { appMiddleware: 'auth' }, }, })
Requesting /users/%61dmin still renders app/pages/users/[id].vue with id decoded to admin, but the rule keyed /users/admin does not match, so auth does not run and the server renders and returns the gated page to an unauthenticated visitor. No privileges and no user interaction are required, and only ASCII characters are involved.
Purely static ASCII routes are not affected: static segments are matched without decoding, so a request for /%61dmin does not resolve to the page registered at /admin and 404s instead of rendering.
The rule keys affected are the ones carried in the app manifest and consumed by the app: appMiddleware, redirect, appLayout, ssr, prerender, and the payload flags derived from cache, isr and swr.
If you have pinned or overridden dependency versions below what your Nuxt release declares, a release outside the affected ranges can still be exposed, so do not conclude from a single reproduction that you are unaffected.
Patches
Releases from 4.4.4 and 3.21.4 are not affected, because their dependency floor requires a version of the underlying HTTP layer that normalises percent-encoding in the request path before routing, which removes the mismatch in a default install.
The comparison itself is fixed in nuxt@4.5.2 and nuxt@3.21.11: route-rule keys and lookup paths are now decoded (and, unless router.options.sensitive is set, case-folded) before they are compared. Reserved escapes such as %2F are left encoded, so decoding cannot introduce extra path segments, and paths that cannot be decoded are matched verbatim. Upgrading to those releases is recommended, since it fixes the comparison rather than relying on a dependency to normalise the input, and it also restores route rules for pages whose paths contain non-ASCII characters.
Workarounds
If you cannot upgrade, do not rely on a routeRules key alone to protect a route. Attach the middleware to the page itself, which binds it to the resolved route rather than to the raw path string:
vue <script setup lang="ts"> definePageMeta({ middleware: 'auth' }) </script>
Credits
Thanks to @sushi-gif, whose report that route rules keyed with non-ASCII paths never match the routes generated for those pages prompted the review that found this issue.
Nuxt is an open-source web development framework for Vue.js. Nuxt from 3.0.0 until 4.6.4, @nuxt/nitro-server from 3.20.0 until 4.6.4, and @nuxt/vite-server from 4.6.0 until 4.6.4, routeRules matching leaves an encoded slash (%2F) in the request path while a catch-all page such as app/pages/[...slug].vue or a custom multi-segment page route decodes it into a route parameter. The getRouteRules result is therefore incomplete, and an unauthenticated request can render the page without the redirect, headers, proxy, SSR, noScripts, appMiddleware, or appLayout rules configured for the decoded path. Protected content is resolved only when the page interprets the decoded parameter accordingly: a custom multi-segment parameter can contain the decoded path, while a repeatable catch-all must join the slash-containing array element; route.path remains encoded and does not resolve that resource. Static pages, single-segment parameters, server/api handlers, and server/routes handlers are not affected because they do not have the same encoded-path mismatch. This issue is fixed in version 4.6.4.
Nuxt from 4.6.0 before 4.6.2 contains a route parameter confusion vulnerability in getRouterParam and getRouterParams that lets attackers return a parameter value different from the one h3 matched by sending paths containing an encoded question mark (%3F). Attackers can craft requests where authorization checks use the Nuxt helper value while resource selection uses event.context.params, allowing access to unauthorized resources.
Nuxt from 4.6.0 before 4.6.2 contains a route parameter confusion vulnerability in getRouterParam and getRouterParams that lets attackers return a parameter value different from the one h3 matched by sending paths containing an encoded question mark (%3F). Attackers can craft requests where authorization checks use the Nuxt helper value while resource selection uses event.context.params, allowing access to unauthorized resources.
Nuxt versions 4.0.0 before 4.4.7 and 3.x before 3.21.7 accept protocol-relative paths such as //evil.com in the reloadNuxtApp function; these pass the script-protocol check but resolve to a cross-origin URL against the current page protocol. Attackers can inject paths like //evil.com to redirect users to attacker-controlled hosts, enabling phishing and OAuth authorization-code theft.
Nuxt versions 4.0.0 before 4.4.7 and 3.x before 3.21.7 fail to validate script-capable URLs in the navigateTo open option, allowing client-side script execution. Attackers can supply javascript: URLs through the open parameter to execute arbitrary scripts in the application's origin when user-controlled input is passed to navigateTo.
Nuxt versions 4.0.0 before 4.4.7 and 3.x before 3.21.7 contain a server-side open redirect vulnerability in navigateTo that fails to properly validate path-normalized payloads like /..//evil.com and /.//evil.com. Attackers can bypass external-host checks using path-normalization techniques to redirect users to attacker-controlled sites via the Location header or meta-refresh, enabling phishing and OAuth authorization-code theft.
Summary
<NuxtLink> did not validate the URL scheme of values bound to its to or href props before rendering them into the href attribute of the underlying <a> element. When an application binds attacker-controlled input (a query parameter, a CMS field, a user-supplied profile URL) to <NuxtLink :to> or :href, the attacker can supply a javascript: or vbscript: URL that is reflected verbatim into the rendered markup. Clicking the link executes the supplied script in the origin of the Nuxt application, resulting in reflected DOM-based cross-site scripting. A data:text/html,... payload reflected through the same sink does not execute in the application's origin but enables a same-tab phishing surface anchored to a legitimate application link.
The same value was exposed to consumers of the component's custom slot via the href and route.href props, so applications that re-bind those values to their own anchors were affected identically.
Unlike the previously reported navigateTo issue (CVE-2024-34343), the sink here is the rendered anchor itself; the existing isScriptProtocol checks in navigateTo and reloadNuxtApp are not on the code path. The onClick handler intentionally returns early for external links so the browser's native protocol-based navigation runs.
Affected component
- File: packages/nuxt/src/app/components/nuxt-link.ts - Sink: h('a', { href: href.value, ... }) in the default render, plus the href / route.href props passed to the custom slot. - Broken check: external auto-detection treated any hasProtocol(path, { acceptRelative: true }) value as an "external link", then rendered the value directly as <a href> without rejecting script-capable protocols. There was no equivalent of the navigateTo isScriptProtocol(protocol) gate in this path.
Impact
Any Nuxt application that binds user-controlled values to <NuxtLink :to> / :href was vulnerable. Common shapes: profile-link rendering (<NuxtLink :to="user.website">), "share this" / "open in new tab" handlers that pass through a query parameter, CMS-driven landing pages that render <NuxtLink :to="cms.cta.url">, and marketplace listings that show seller-supplied links.
For javascript: / vbscript: the primitive is reflected XSS in the application's first-party origin (session theft for non-HttpOnly cookies, CSRF token theft, account takeover via DOM rewriting, credential harvesting via fake login overlays). For data:text/html,... the attacker gets a same-tab phishing surface anchored to a legitimate application link.
Patches
Fixed in nuxt@4.4.7 (commit 0103ce06) and backported to nuxt@3.21.7 (commit 53284043). The fix sanitises the resolved external href before it is passed to <a> or the custom slot: control characters and whitespace are stripped, leading view-source: prefixes are unwrapped, and any remaining script-capable scheme (per isScriptProtocol) causes the href to be replaced with an empty string.
Workarounds
Until you can upgrade, validate URLs at the source before binding them to <NuxtLink :to> / :href. For example, only accept paths that start with / (and not //), or run user-supplied URLs through new URL(value) and reject anything whose protocol is not in an allow-list (typically http: and https:).
Nuxt before 4.4.7 (and the 3.x branch before 3.21.7) contains a cross-site scripting vulnerability in the NoScript component that writes slot content to innerHTML without escaping. Attackers can inject malicious scripts through untrusted data in NoScript slots, such as route.query parameters, which execute in the document context when the noscript tag is implicitly closed by script tags.
Impact
An application that renders <NuxtLink :to="..."> with a value it does not control can be made to render an anchor whose href executes script in the application's origin when clicked.
This is an incomplete fix for GHSA-934w-87qh-qr26 (CVE-2026-53722). It only affects applications running without a pages/ directory, where pages resolves to false and Nuxt registers its own router in place of vue-router. Applications using vue-router are not affected.
Prefixing javascript: with an ASCII C0 control character, for example %01, meant the value was not recognised as having a protocol. It was classified as an internal route and so never reached the protocol check added in GHSA-934w-87qh-qr26, and the router used for pages: false passed it through to the rendered href unchanged. Browsers strip the leading control character and run the result as a script URL.
Both <NuxtLink custom> and the default anchor are affected, and the href is present in the server-rendered HTML.
Mitigating factors
- The application must have pages disabled (no pages/ directory, or pages: false). - The application must bind untrusted input to :to. - A victim must click the rendered link.
Affected versions
nuxt <4.6.0, including all Nuxt 3 releases. Versions before 4.4.7 and 3.21.7 are also affected by GHSA-934w-87qh-qr26.
Patches
Fixed in nuxt@4.6.0. Nuxt 3 reached end of life on 31 July 2026 and will not receive a fix.
Anchor href values are now checked for script-capable protocols wherever they are rendered, including the router used when pages is disabled. Protocol detection also moved to the platform URL parser, so a URL is classified the same way the browser will read it.
Workarounds
Upgrade to nuxt@4.6.0 or later. If you cannot upgrade, including on Nuxt 3, validate values before binding them to :to, for example by rejecting anything that is not a path beginning with a single /.