@@ -386,14 +386,27 @@ pub async fn download_package_manager(
386386 version_or_latest. into ( )
387387 } ;
388388
389+ // Reject anything that is not strict semver `major.minor.patch[-prerelease][+build]`.
390+ // This prevents path traversal via the version being interpolated into
391+ // `$VP_HOME/package_manager/{name}/{version}` below, since `AbsolutePath::join`
392+ // does not normalize `..` components. Also guards against registry-controlled
393+ // "latest" lookups returning a malicious value.
394+ let parsed_version = Version :: parse ( & version) . map_err ( |_| {
395+ Error :: InvalidArgument (
396+ format ! (
397+ "invalid {package_manager_type} version {version:?}: expected semver 'major.minor.patch'"
398+ )
399+ . into ( ) ,
400+ )
401+ } ) ?;
402+
389403 let mut package_name: Str = package_manager_type. to_string ( ) . into ( ) ;
390404 // handle yarn >= 2.0.0 to use `@yarnpkg/cli-dist` as package name
391405 // @see https://github.com/nodejs/corepack/blob/main/config.json#L135
392- if matches ! ( package_manager_type, PackageManagerType :: Yarn ) {
393- let version_req = VersionReq :: parse ( ">=2.0.0" ) ?;
394- if version_req. matches ( & Version :: parse ( & version) ?) {
395- package_name = "@yarnpkg/cli-dist" . into ( ) ;
396- }
406+ if matches ! ( package_manager_type, PackageManagerType :: Yarn )
407+ && VersionReq :: parse ( ">=2.0.0" ) ?. matches ( & parsed_version)
408+ {
409+ package_name = "@yarnpkg/cli-dist" . into ( ) ;
397410 }
398411
399412 let home_dir = vite_shared:: get_vp_home ( ) ?;
@@ -1840,6 +1853,24 @@ mod tests {
18401853 assert ! ( result. get_bin_prefix( ) . ends_with( "pnpm/bin" ) ) ;
18411854 }
18421855
1856+ #[ tokio:: test]
1857+ async fn test_download_package_manager_rejects_path_traversal_version ( ) {
1858+ // Versions containing path separators or traversal components must be
1859+ // rejected before any filesystem operations: `AbsolutePath::join` does
1860+ // not normalize `..`, so a bad version would escape the home dir.
1861+ for bad in [ "../../../escape" , ".." , "1.0.0/../../escape" , "/foo/bar" , "1.0.0\0 " , "" ] {
1862+ let result = download_package_manager ( PackageManagerType :: Pnpm , bad, None ) . await ;
1863+ match result {
1864+ Err ( Error :: InvalidArgument ( _) ) => { }
1865+ other => panic ! ( "expected InvalidArgument for {bad:?}, got {other:?}" ) ,
1866+ }
1867+ }
1868+
1869+ // Bun takes a separate code path but shares the same pre-validation.
1870+ let result = download_package_manager ( PackageManagerType :: Bun , "../../escape" , None ) . await ;
1871+ assert ! ( matches!( result, Err ( Error :: InvalidArgument ( _) ) ) ) ;
1872+ }
1873+
18431874 #[ tokio:: test]
18441875 async fn test_download_package_manager ( ) {
18451876 let result = download_package_manager ( PackageManagerType :: Yarn , "4.9.2" , None ) . await ;
0 commit comments