Skip to content

Commit e159b84

Browse files
authored
fix(install): reject non-semver package manager versions (#1386)
Strictly validate the resolved version in `download_package_manager` before it is interpolated into `$VP_HOME/package_manager/{name}/{version}`. `AbsolutePath::join` does not normalize `..`, so a version containing path components could escape the home directory. The check also covers registry-controlled `latest` lookups. fixes [GHSA-33r3-4whc-44c2](GHSA-33r3-4whc-44c2)
1 parent 48e49ca commit e159b84

File tree

1 file changed

+36
-5
lines changed

1 file changed

+36
-5
lines changed

crates/vite_install/src/package_manager.rs

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)