---
id: "GHSA-f3x9-2chm-73c8"
page: "https://infrastack.ai/live/GHSA-f3x9-2chm-73c8"
kind: "vulnerability"
aliases: ["GHSA-f3x9-2chm-73c8"]
title: "Incomplete fix for GHSA-6gq8-jgmh-3ffp: an encoded slash (`%2F`) in the request path bypasses route rules on catch-all page routes"
severity: "medium"
cvss: 6.5
epss: null
exploited: false
status: "open"
category: "package"
products: [{"name": "nuxt", "fixed": ["4.6.4"], "affected": ">= 4.0.0, < 4.6.4", "ecosystem": "npm"}, {"name": "nuxt", "fixed": [], "affected": ">= 3.0.0, < 4.0.0", "ecosystem": "npm"}, {"name": "@nuxt/nitro-server", "fixed": ["4.6.4"], "affected": ">= 4.2.0, < 4.6.4", "ecosystem": "npm"}, {"name": "@nuxt/nitro-server", "fixed": [], "affected": ">= 3.20.0, < 4.0.0", "ecosystem": "npm"}, {"name": "@nuxt/vite-server", "fixed": ["4.6.4"], "affected": ">= 4.6.0, < 4.6.4", "ecosystem": "npm"}]
ecosystems: ["npm"]
tags: ["cwe-436", "cwe-863", "netlify", "nuxt", "package", "self-hosted", "vercel"]
not_affected_if: ["api-only", "no-route-rules", "static-export"]
published_at: "2026-11-02T20:56:05+00:00"
first_seen_at: "2026-11-09T22:53:16+00:00"
updated_at: "2026-11-09T22:54:42+00:00"
sources: [{"source": "ghsa-repo", "url": "https://github.com/nuxt/nuxt/security/advisories/GHSA-f3x9-2chm-73c8", "first_seen_at": "2026-11-09T22:53:16+00:00"}]
exploit_sources: []
---

# Incomplete fix for GHSA-6gq8-jgmh-3ffp: an encoded slash (`%2F`) in the request path bypasses route rules on catch-all page routes

Link for people: https://infrastack.ai/live/GHSA-f3x9-2chm-73c8 (this .md is for agents)

Nuxt matches route rules against the raw request path, so a %2F in the path is not treated as a segment separator while the page router decodes it. An unauthenticated attacker can reach a catch-all page without the auth, redirect or header rules configured for that path.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- npm:nuxt affected >= 4.0.0, < 4.6.4 fixed 4.6.4
- npm:nuxt affected >= 3.0.0, < 4.0.0 fixed no fixed version listed
- npm:@nuxt/nitro-server affected >= 4.2.0, < 4.6.4 fixed 4.6.4
- npm:@nuxt/nitro-server affected >= 3.20.0, < 4.0.0 fixed no fixed version listed
- npm:@nuxt/vite-server affected >= 4.6.0, < 4.6.4 fixed 4.6.4

## How to check

- Check the installed Nuxt version with `npm ls nuxt` and confirm it is below `4.6.4`
- Look for `routeRules` in `nuxt.config.ts` that protect paths served by a catch-all page such as `app/pages/[...slug].vue`
- Check whether `nuxt` 3.x is still in use, since the advisory says Nuxt 3 will not receive a fix

## Fix

- `nuxt` `4.6.4` for the 4.x line
- `@nuxt/nitro-server` `4.6.4` and `@nuxt/vite-server` `4.6.4`
- Nuxt 3 has reached end of life and receives no fix

## Timeline

- 2026-11-09T22:53:16+00:00 ghsa-repo https://github.com/nuxt/nuxt/security/advisories/GHSA-f3x9-2chm-73c8

## Documents

- [advisory] Maintainer's advisory: github.com https://github.com/nuxt/nuxt/security/advisories/GHSA-f3x9-2chm-73c8

## From the sources

### Summary

Nuxt matches route rules against the request path with encoded slashes (`%2F`) left encoded, while the page router decodes them into route params. A request that spells a slash as `%2F` can therefore render a page through a catch-all route without the route rules configured for the path it resolves to. The fix for [GHSA-6gq8-jgmh-3ffp](https://github.com/nuxt/nuxt/security/advisories/GHSA-6gq8-jgmh-3ffp) deliberately left reserved escapes such as `%2F` encoded, so it did not cover this form.

### Impact

An unauthenticated attacker can bypass route rules that protect paths served by a catch-all page route, or by a custom route whose param can span several segments.

```ts
export default defineNuxtConfig({
  routeRules: {
    '/docs/internal/**': { appMiddleware: 'auth', headers: { 'X-Frame-Options': 'DENY' } },
    '/admin/**': { redirect: '/login' },
  },
})
```

With `app/pages/docs/[...slug].vue`, `/docs/internal/guide` runs `auth`, but `/docs/internal%2Fguide` renders the same page without running `auth` and without the header. With a root `app/pages/[...slug].vue`, `/admin%2Fsecret` is rendered instead of redirected.

The rules skipped include server-side `redirect`, `headers`, `proxy`, `ssr` and `noScripts`, and `appMiddleware`, `appLayout` and `redirect` as matched in the app during server rendering and client navigation. `getRouteRules` returns the same incomplete result.

Whether the page then shows protected content depends on how it reads the route:

- With a route such as `definePageMeta({ path: '/docs/:path(.*)' })`, `route.params.path` is `internal/guide` for both requests.
- With a repeatable catch-all (`[...slug].vue`), `route.params.slug` is `['internal/guide']` instead of `['internal', 'guide']`, so a page that joins the segments (`slug.join('/')`) to load content loads the protected resource.
- `route.path` keeps the encoding (`/docs/internal%2Fguide`), so a page that loads content by `route.path` does not resolve it.

Not affected:

- Static page routes and params that match a single segment. `/docs%2Finternal` does not match `app/pages/docs/internal.vue`.
- API handlers in `server/api` and `server/routes`. As far as we can tell, Nitro matches these against the same path as its route rules, so a handler and the rules for it agree.

Browsers send `%2F` in a path as written, so a plain link is enough to reach the page.

### Patches

Fixed in `nuxt@4.6.4`, together with `@nuxt/nitro-server@4.6.4` and `@nuxt/vite-server@4.6.4`, which it depends on. Route rules are now resolved for both the request path and the path with each `%2F` read as a separator, and the rules matched by either apply. Nuxt 3 has reached end of life and will not receive a fix.

### Workarounds

Reject page requests whose path contains an encoded slash in a server middleware:

```ts
// server/middleware/encoded-slash.ts
export default defineEventHandler((event) => {
  if (/%2f/i.test(event.path.split('?')[0])) {
    throw createError({ statusCode: 404 })
  }
})
```

This also rejects legitimate uses, such as an API route that takes a scoped package name (`@scope%2Fname`) as a param; narrow the check to the path prefixes your route rules protect if you need those.

A reverse proxy or CDN that rejects or decodes encoded slashes before forwarding the request also removes the mismatch.

### Credits

Thanks to Riahi Foudhil for reporting this issue.

---

_Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai_
