{"id":"GHSA-f3x9-2chm-73c8","kind":"vulnerability","aliases":["GHSA-f3x9-2chm-73c8"],"related":[],"title":"Incomplete fix for GHSA-6gq8-jgmh-3ffp: an encoded slash (`%2F`) in the request path bypasses route rules on catch-all page routes","description":"### Summary\r\n\r\nNuxt 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.\r\n\r\n### Impact\r\n\r\nAn 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.\r\n\r\n```ts\r\nexport default defineNuxtConfig({\r\n  routeRules: {\r\n    '/docs/internal/**': { appMiddleware: 'auth', headers: { 'X-Frame-Options': 'DENY' } },\r\n    '/admin/**': { redirect: '/login' },\r\n  },\r\n})\r\n```\r\n\r\nWith `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.\r\n\r\nThe 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.\r\n\r\nWhether the page then shows protected content depends on how it reads the route:\r\n\r\n- With a route such as `definePageMeta({ path: '/docs/:path(.*)' })`, `route.params.path` is `internal/guide` for both requests.\r\n- 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.\r\n- `route.path` keeps the encoding (`/docs/internal%2Fguide`), so a page that loads content by `route.path` does not resolve it.\r\n\r\nNot affected:\r\n\r\n- Static page routes and params that match a single segment. `/docs%2Finternal` does not match `app/pages/docs/internal.vue`.\r\n- 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.\r\n\r\nBrowsers send `%2F` in a path as written, so a plain link is enough to reach the page.\r\n\r\n### Patches\r\n\r\nFixed 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.\r\n\r\n### Workarounds\r\n\r\nReject page requests whose path contains an encoded slash in a server middleware:\r\n\r\n```ts\r\n// server/middleware/encoded-slash.ts\r\nexport default defineEventHandler((event) => {\r\n  if (/%2f/i.test(event.path.split('?')[0])) {\r\n    throw createError({ statusCode: 404 })\r\n  }\r\n})\r\n```\r\n\r\nThis 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.\r\n\r\nA reverse proxy or CDN that rejects or decodes encoded slashes before forwarding the request also removes the mismatch.\r\n\r\n### Credits\r\n\r\nThanks to Riahi Foudhil for reporting this issue.","summary":"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.","severity":"medium","cvss":6.5,"epss":null,"exploited":false,"exploited_at":null,"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"],"how_to_check":"- Check the installed Nuxt version with `npm ls nuxt` and confirm it is below `4.6.4`\n- Look for `routeRules` in `nuxt.config.ts` that protect paths served by a catch-all page such as `app/pages/[...slug].vue`\n- 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\n- `@nuxt/nitro-server` `4.6.4` and `@nuxt/vite-server` `4.6.4`\n- Nuxt 3 has reached end of life and receives no fix","status":"open","published_at":"2026-11-02T20:56:05+00:00","first_seen_at":"2026-11-09T22:53:16+00:00","triaged_at":"2026-11-09T22:54:42+00:00","updated_at":"2026-11-09T22:54:42+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-11-09T22:54:42Z","model":"claude-haiku-5-5","output":{"fix":"- `nuxt` `4.6.4` for the 4.x line\n- `@nuxt/nitro-server` `4.6.4` and `@nuxt/vite-server` `4.6.4`\n- Nuxt 3 has reached end of life and receives no fix","reach":"internet","title":"Nuxt: encoded slash in request path skips route rules on catch-all pages","impact":"medium","summary":"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.","audience":"developers","category":"package","nicknames":[],"who_cares":["nuxt","self-hosted","vercel","netlify"],"confidence":0.8,"fix_status":"available","needs_auth":"no","worst_case":"protected pages render and protected content loads without auth","analyst_note":"","how_to_check":"- Check the installed Nuxt version with `npm ls nuxt` and confirm it is below `4.6.4`\n- Look for `routeRules` in `nuxt.config.ts` that protect paths served by a catch-all page such as `app/pages/[...slug].vue`\n- Check whether `nuxt` 3.x is still in use, since the advisory says Nuxt 3 will not receive a fix","plain_action":"","product_name":"","needs_analyst":false,"not_affected_if":["static-export","no-route-rules","api-only"],"who_is_affected":"Applications built with Nuxt 4 that use routeRules to protect paths served by a catch-all page route or a custom route whose param can span several segments. Static routes and API handlers in server/api and server/routes are not affected according to the advisory. Teams on Nuxt 3 have no upstream fix and need a workaround.","user_interaction":"no","product_ecosystem":"","needs_special_setup":"yes","severity_if_unknown":"medium"},"cost_usd":0.000938,"input_tokens":6018,"output_tokens":672,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-436':42B '-863':44B '/admin':225C,255C '/docs':318C,386C '/docs/internal':216C,236C,360C '/docs/internal/guide':232C '/login':227C '/nuxt/nuxt/security/advisories/ghsa-6gq8-jgmh-3ffp)':165C '0':521C '2chm':29A,34A '2f':12A,63B,113C,132C,172C,426C,473C '2f/i.test':519C '2fguide':237C,361C '2finternal':387C '2fname':543C '2fsecret':256C '3':486C '3ffp':8A,162C '4.6.4':446C,450C,453C '404':525C '6gq8':6A,160C '73c8':30A,35A 'affect':375C 'agre':423C 'also':527C,577C 'api':392C,534C 'app':284C 'app/pages':252C 'app/pages/docs':229C 'app/pages/docs/internal.vue':391C 'applayout':277C 'appli':484C 'appmiddlewar':217C,276C 'attack':82B,184C 'auth':92B,218C,234C,244C 'browser':424C 'bypass':17A,186C 'catch':22A,87B,141C,196C,330C 'catch-al':21A,86B,140C,195C,329C 'cdn':566C 'check':549C 'client':289C 'configur':97B,148C 'contain':504C 'content':303C,351C,367C 'cover':178C 'createerror':523C 'credit':581C 'custom':203C 'cwe':41B,43B 'decod':78B,120C,570C 'default':213C,515C 'defineeventhandl':516C 'definenuxtconfig':214C 'definepagemeta':316C 'deliber':166C 'deni':224C 'depend':304C,456C 'either':483C 'encod':10A,111C,115C,173C,359C,506C,571C 'end':489C 'enough':437C 'escap':169C 'event':517C 'event.path.split':520C 'export':212C,514C 'f3x9':28A,33A 'far':399C 'fix':2A,156C,443C,497C 'form':180C 'forward':574C 'foudhil':585C 'frame':222C 'getrouterul':291C 'ghsa':5A,27A,32A,159C 'ghsa-6gq8-jgmh-3ffp':4A,158C 'ghsa-f3x9-2chm-73c8':26A,31A 'github.com':164C 'github.com/nuxt/nuxt/security/advisories/ghsa-6gq8-jgmh-3ffp)':163C 'guid':340C 'handler':393C,417C 'header':95B,219C,248C,270C 'host':50B 'impact':181C 'includ':265C 'incomplet':1A,295C 'instead':259C,337C 'intern':339C 'internal/guide':322C,336C 'issu':589C 'jgmh':7A,161C 'join':345C 'keep':357C 'left':114C,167C 'legitim':529C 'life':491C 'link':435C 'load':350C,352C,366C 'match':53B,103C,281C,382C,390C,405C,481C 'middlewar':511C 'mismatch':580C 'name':541C 'narrow':547C 'navig':290C 'need':560C 'netlifi':45B 'nitro':404C 'noscript':274C 'nuxt':36A,37A,46B,52B,102C,445C,485C 'nuxt/nitro-server':38A,39A,449C 'nuxt/vite-server':40A,452C 'option':223C 'packag':47B,540C 'page':24A,76B,89B,118C,137C,198C,241C,299C,343C,364C,377C,441C,500C 'param':124C,206C,380C,546C 'patch':442C 'path':16A,60B,66B,100B,109C,151C,191C,317C,319C,410C,429C,467C,470C,503C,552C 'plain':434C 'prefix':553C 'protect':190C,302C,354C,557C 'proxi':271C,564C 'raw':58B 'reach':84B,439C,488C 'read':308C,474C 'receiv':495C 'redirect':93B,226C,261C,269C,279C 'reject':499C,528C,568C 'remov':578C 'render':135C,238C,258C,287C 'repeat':328C 'report':587C 'request':15A,59B,108C,126C,325C,466C,501C,576C 'reserv':168C 'resolv':153C,372C,462C 'resourc':355C 'result':296C 'return':292C 'revers':563C 'riahi':584C 'root':251C 'rout':18A,25A,54B,104C,123C,143C,146C,187C,199C,204C,310C,313C,378C,413C,458C,535C,555C 'route.params.path':320C 'route.params.slug':334C 'route.path':356C,369C 'router':77B,119C 'routerul':215C 'rule':19A,55B,96B,105C,147C,188C,263C,414C,420C,459C,480C,556C 'run':233C,243C 'scope':539C,542C 'segment':72B,210C,347C,385C 'self':49B 'self-host':48B 'send':425C 'separ':73B,477C 'serv':192C 'server':267C,286C,510C 'server-sid':266C 'server/api':395C 'server/middleware/encoded-slash.ts':513C 'server/routes':397C 'sever':209C 'show':301C 'side':268C 'singl':384C 'skip':264C 'slash':11A,112C,130C,507C,572C 'slug':230C,253C,332C 'slug.join':348C 'span':208C 'spell':128C 'ssr':272C 'static':376C 'statuscod':524C 'summari':101C 'take':537C 'tell':403C 'thank':582C 'therefor':134C 'throw':522C 'togeth':447C 'treat':69B 'ts':211C,512C 'unauthent':81B,183C 'use':530C 'vercel':51B 'vue':231C,254C,333C 'whether':297C 'whose':205C,502C 'without':90B,144C,242C,246C 'workaround':498C 'written':431C 'x':221C 'x-frame-opt':220C","nicknames":[],"mitigations":[],"who_affected":"Applications built with Nuxt 4 that use routeRules to protect paths served by a catch-all page route or a custom route whose param can span several segments. Static routes and API handlers in server/api and server/routes are not affected according to the advisory. Teams on Nuxt 3 have no upstream fix and need a workaround.","tier":"C","verified":{},"analyst_revisit_at":null,"fix_status":"available","audience":"developers","plain_action":null,"pkeys":["nitroserver","nuxt","viteserver"],"ptokens":["nitro","nuxt","server","vite"],"labels":["cwe436","cwe863","medium","netlify","npm","nuxt","package","selfhosted","vercel"],"campaign_type":null,"search_a":"'2chm':29A,34A '2f':12A '3ffp':8A '6gq8':6A '73c8':30A,35A 'bypass':17A 'catch':22A 'catch-al':21A 'encod':10A 'f3x9':28A,33A 'fix':2A 'ghsa':5A,27A,32A 'ghsa-6gq8-jgmh-3ffp':4A 'ghsa-f3x9-2chm-73c8':26A,31A 'incomplet':1A 'jgmh':7A 'nuxt':36A,37A 'nuxt/nitro-server':38A,39A 'nuxt/vite-server':40A 'page':24A 'path':16A 'request':15A 'rout':18A,25A 'rule':19A 'slash':11A","search_b":"'-436':42B '-863':44B '2f':63B 'attack':82B 'auth':92B 'catch':87B 'catch-al':86B 'configur':97B 'cwe':41B,43B 'decod':78B 'header':95B 'host':50B 'match':53B 'netlifi':45B 'nuxt':46B,52B 'packag':47B 'page':76B,89B 'path':60B,66B,100B 'raw':58B 'reach':84B 'redirect':93B 'request':59B 'rout':54B 'router':77B 'rule':55B,96B 'segment':72B 'self':49B 'self-host':48B 'separ':73B 'treat':69B 'unauthent':81B 'vercel':51B 'without':90B","search_c":"'/admin':225C,255C '/docs':318C,386C '/docs/internal':216C,236C,360C '/docs/internal/guide':232C '/login':227C '/nuxt/nuxt/security/advisories/ghsa-6gq8-jgmh-3ffp)':165C '0':521C '2f':113C,132C,172C,426C,473C '2f/i.test':519C '2fguide':237C,361C '2finternal':387C '2fname':543C '2fsecret':256C '3':486C '3ffp':162C '4.6.4':446C,450C,453C '404':525C '6gq8':160C 'affect':375C 'agre':423C 'also':527C,577C 'api':392C,534C 'app':284C 'app/pages':252C 'app/pages/docs':229C 'app/pages/docs/internal.vue':391C 'applayout':277C 'appli':484C 'appmiddlewar':217C,276C 'attack':184C 'auth':218C,234C,244C 'browser':424C 'bypass':186C 'catch':141C,196C,330C 'catch-al':140C,195C,329C 'cdn':566C 'check':549C 'client':289C 'configur':148C 'contain':504C 'content':303C,351C,367C 'cover':178C 'createerror':523C 'credit':581C 'custom':203C 'decod':120C,570C 'default':213C,515C 'defineeventhandl':516C 'definenuxtconfig':214C 'definepagemeta':316C 'deliber':166C 'deni':224C 'depend':304C,456C 'either':483C 'encod':111C,115C,173C,359C,506C,571C 'end':489C 'enough':437C 'escap':169C 'event':517C 'event.path.split':520C 'export':212C,514C 'far':399C 'fix':156C,443C,497C 'form':180C 'forward':574C 'foudhil':585C 'frame':222C 'getrouterul':291C 'ghsa':159C 'ghsa-6gq8-jgmh-3ffp':158C 'github.com':164C 'github.com/nuxt/nuxt/security/advisories/ghsa-6gq8-jgmh-3ffp)':163C 'guid':340C 'handler':393C,417C 'header':219C,248C,270C 'impact':181C 'includ':265C 'incomplet':295C 'instead':259C,337C 'intern':339C 'internal/guide':322C,336C 'issu':589C 'jgmh':161C 'join':345C 'keep':357C 'left':114C,167C 'legitim':529C 'life':491C 'link':435C 'load':350C,352C,366C 'match':103C,281C,382C,390C,405C,481C 'middlewar':511C 'mismatch':580C 'name':541C 'narrow':547C 'navig':290C 'need':560C 'nitro':404C 'noscript':274C 'nuxt':102C,445C,485C 'nuxt/nitro-server':449C 'nuxt/vite-server':452C 'option':223C 'packag':540C 'page':118C,137C,198C,241C,299C,343C,364C,377C,441C,500C 'param':124C,206C,380C,546C 'patch':442C 'path':109C,151C,191C,317C,319C,410C,429C,467C,470C,503C,552C 'plain':434C 'prefix':553C 'protect':190C,302C,354C,557C 'proxi':271C,564C 'reach':439C,488C 'read':308C,474C 'receiv':495C 'redirect':226C,261C,269C,279C 'reject':499C,528C,568C 'remov':578C 'render':135C,238C,258C,287C 'repeat':328C 'report':587C 'request':108C,126C,325C,466C,501C,576C 'reserv':168C 'resolv':153C,372C,462C 'resourc':355C 'result':296C 'return':292C 'revers':563C 'riahi':584C 'root':251C 'rout':104C,123C,143C,146C,187C,199C,204C,310C,313C,378C,413C,458C,535C,555C 'route.params.path':320C 'route.params.slug':334C 'route.path':356C,369C 'router':119C 'routerul':215C 'rule':105C,147C,188C,263C,414C,420C,459C,480C,556C 'run':233C,243C 'scope':539C,542C 'segment':210C,347C,385C 'send':425C 'separ':477C 'serv':192C 'server':267C,286C,510C 'server-sid':266C 'server/api':395C 'server/middleware/encoded-slash.ts':513C 'server/routes':397C 'sever':209C 'show':301C 'side':268C 'singl':384C 'skip':264C 'slash':112C,130C,507C,572C 'slug':230C,253C,332C 'slug.join':348C 'span':208C 'spell':128C 'ssr':272C 'static':376C 'statuscod':524C 'summari':101C 'take':537C 'tell':403C 'thank':582C 'therefor':134C 'throw':522C 'togeth':447C 'ts':211C,512C 'unauthent':183C 'use':530C 'vue':231C,254C,333C 'whether':297C 'whose':205C,502C 'without':144C,242C,246C 'workaround':498C 'written':431C 'x':221C 'x-frame-opt':220C","cause_entry":null,"cause_path":null,"disclosures_checked_at":null,"first_public_at":"2026-11-02T20:56:05+00:00","kev":null,"cvss_vector":null,"cvss_by":null,"poc_count":0,"sources":[{"source":"ghsa-repo","external_id":"GHSA-f3x9-2chm-73c8","url":"https://github.com/nuxt/nuxt/security/advisories/GHSA-f3x9-2chm-73c8","severity":"medium","published_at":"2026-11-02T20:56:05+00:00","modified_at":"2026-11-02T20:56:05+00:00","first_seen_at":"2026-11-09T22:53:16+00:00","fetched_at":"2026-11-09T22:53:16+00:00","note":null}],"documents":[{"id":110433,"url":"https://github.com/nuxt/nuxt/security/advisories/GHSA-f3x9-2chm-73c8","title":"Maintainer's advisory: github.com","kind":"vendor","source":"ghsa-repo","publisher":"vendor","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Maintainer's advisory: github.com"}],"disclosures":[],"exploit_sources":[],"notice":"Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai"}