1- import { proxyRequest , redirect as sendRedirect , requireBasicAuth } from "h3" ;
1+ import { HTTPError , proxyRequest , redirect as sendRedirect , requireBasicAuth } from "h3" ;
22import type { BasicAuthOptions , EventHandler , Middleware } from "h3" ;
33import type { MatchedRouteRule , NitroRouteRules } from "nitro/types" ;
44import { joinURL , withQuery , withoutBase } from "ufo" ;
@@ -29,6 +29,9 @@ export const redirect: RouteRuleCtor<"redirect"> = ((m) =>
2929 let targetPath = event . url . pathname + event . url . search ;
3030 const strpBase = ( m . options as any ) . _redirectStripBase ;
3131 if ( strpBase ) {
32+ if ( ! isPathInScope ( event . url . pathname , strpBase ) ) {
33+ throw new HTTPError ( { status : 400 } ) ;
34+ }
3235 targetPath = withoutBase ( targetPath , strpBase ) ;
3336 }
3437 target = joinURL ( target . slice ( 0 , - 3 ) , targetPath ) ;
@@ -49,6 +52,9 @@ export const proxy: RouteRuleCtor<"proxy"> = ((m) =>
4952 let targetPath = event . url . pathname + event . url . search ;
5053 const strpBase = ( m . options as any ) . _proxyStripBase ;
5154 if ( strpBase ) {
55+ if ( ! isPathInScope ( event . url . pathname , strpBase ) ) {
56+ throw new HTTPError ( { status : 400 } ) ;
57+ }
5258 targetPath = withoutBase ( targetPath , strpBase ) ;
5359 }
5460 target = joinURL ( target . slice ( 0 , - 3 ) , targetPath ) ;
@@ -96,3 +102,22 @@ export const basicAuth: RouteRuleCtor<"auth"> = /* @__PURE__ */ Object.assign(
96102 } ) satisfies RouteRuleCtor < "auth" > ,
97103 { order : - 1 }
98104) ;
105+
106+ // Check whether `pathname`, after canonicalization, stays within `base`.
107+ // Prevents match/forward differentials where an encoded traversal like `..%2f`
108+ // bypasses the `/**` scope at match time but escapes the base once the
109+ // downstream (proxy upstream or redirect target) decodes `%2f` → `/`
110+ // (GHSA-5w89-w975-hf9q).
111+ //
112+ // WHATWG URL keeps `%2F` and `%5C` opaque in paths, so we pre-decode those,
113+ // then let `new URL` resolve `.`/`..`/`%2E%2E` segments and normalize `\`.
114+ export function isPathInScope ( pathname : string , base : string ) : boolean {
115+ let canonical : string ;
116+ try {
117+ const pre = pathname . replace ( / % 2 f / gi, "/" ) . replace ( / % 5 c / gi, "\\" ) ;
118+ canonical = new URL ( pre , "http://_" ) . pathname ;
119+ } catch {
120+ return false ;
121+ }
122+ return ! base || canonical === base || canonical . startsWith ( base + "/" ) ;
123+ }
0 commit comments