From c4d63a44234de22dc745231c416b80ed848d997f Mon Sep 17 00:00:00 2001 From: Arlo Siemsen Date: Mon, 25 May 2026 09:49:43 +0200 Subject: [PATCH 1/2] CVE-2026-5222: avoid stripping .git suffix when for non git registries --- src/cargo/sources/git/source.rs | 7 ++++++ src/cargo/util/canonical_url.rs | 44 ++++++++++++++++++--------------- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/src/cargo/sources/git/source.rs b/src/cargo/sources/git/source.rs index 1db73a1b9b3..5d26b46a98e 100644 --- a/src/cargo/sources/git/source.rs +++ b/src/cargo/sources/git/source.rs @@ -501,6 +501,13 @@ mod test { assert_eq!(ident1, ident2); } + #[test] + fn test_canonicalize_idents_does_not_strip_dot_git_for_sparse() { + let ident1 = ident(&src("sparse+https://crates.io/fake-registry")); + let ident2 = ident(&src("sparse+https://crates.io/fake-registry.git")); + assert_ne!(ident1, ident2); + } + fn src(s: &str) -> SourceId { SourceId::for_git(&s.into_url().unwrap(), GitReference::DefaultBranch).unwrap() } diff --git a/src/cargo/util/canonical_url.rs b/src/cargo/util/canonical_url.rs index 7516e035691..2716d2d4f5b 100644 --- a/src/cargo/util/canonical_url.rs +++ b/src/cargo/util/canonical_url.rs @@ -33,27 +33,31 @@ impl CanonicalUrl { url.path_segments_mut().unwrap().pop_if_empty(); } - // For GitHub URLs specifically, just lower-case everything. GitHub - // treats both the same, but they hash differently, and we're gonna be - // hashing them. This wants a more general solution, and also we're - // almost certainly not using the same case conversion rules that GitHub - // does. (See issue #84) - if url.host_str() == Some("github.com") { - url = format!("https{}", &url[url::Position::AfterScheme..]) - .parse() - .unwrap(); - let path = url.path().to_lowercase(); - url.set_path(&path); - } + // Perform further canonicalization specific to git registries, which + // do not contain a `+` specifier. + if !url.scheme().contains('+') { + // For GitHub URLs specifically, just lower-case everything. GitHub + // treats both the same, but they hash differently, and we're gonna be + // hashing them. This wants a more general solution, and also we're + // almost certainly not using the same case conversion rules that GitHub + // does. (See issue #84) + if url.host_str() == Some("github.com") { + url = format!("https{}", &url[url::Position::AfterScheme..]) + .parse() + .unwrap(); + let path = url.path().to_lowercase(); + url.set_path(&path); + } - // Repos can generally be accessed with or without `.git` extension. - let needs_chopping = url.path().ends_with(".git"); - if needs_chopping { - let last = { - let last = url.path_segments().unwrap().next_back().unwrap(); - last[..last.len() - 4].to_owned() - }; - url.path_segments_mut().unwrap().pop().push(&last); + // Repos can generally be accessed with or without `.git` extension. + let needs_chopping = url.path().ends_with(".git"); + if needs_chopping { + let last = { + let last = url.path_segments().unwrap().next_back().unwrap(); + last[..last.len() - 4].to_owned() + }; + url.path_segments_mut().unwrap().pop().push(&last); + } } Ok(CanonicalUrl(url)) From 285cebf58911eca5b7f177f5d0b1c53e1f646577 Mon Sep 17 00:00:00 2001 From: Josh Triplett Date: Mon, 30 Mar 2026 10:35:55 -0700 Subject: [PATCH 2/2] CVE-2026-5223: prohibit unpacking symlinks and other unexpected entries Cargo has historically not allowed creating .crate packages containing symlinks. (It packages the symlink target in place of the symlink, instead.) So, any package containing a symlink would have to be hand-constructed. Such packages are also not allowed on crates.io, so it could only come from an alternate registry. Rather than dealing with symlink traversal attacks when unpacking a crate, just prohibit symlinks entirely. In the process, also prohibit other kinds of unusual entries. As an exception, allow character devices but warn about them, because some exist in crates on crates.io. --- src/cargo/sources/registry/mod.rs | 10 +++++++- tests/testsuite/registry.rs | 39 ++++++++++++++----------------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/cargo/sources/registry/mod.rs b/src/cargo/sources/registry/mod.rs index 24170b63cbb..7aeef508bb4 100644 --- a/src/cargo/sources/registry/mod.rs +++ b/src/cargo/sources/registry/mod.rs @@ -198,7 +198,7 @@ use flate2::read::GzDecoder; use futures::FutureExt as _; use serde::Deserialize; use serde::Serialize; -use tar::Archive; +use tar::{Archive, EntryType}; use tracing::debug; use crate::core::dependency::Dependency; @@ -1019,6 +1019,14 @@ fn unpack( ) } + // Prevent unpacking symlinks and other unexpected entry types + match entry.header().entry_type() { + EntryType::Regular | EntryType::Directory => {} + t => anyhow::bail!( + "invalid tarball downloaded, contains an entry at {entry_path:?} with invalid type {t:?}", + ), + } + // Prevent unpacking the lockfile from the crate itself. if entry_path .file_name() diff --git a/tests/testsuite/registry.rs b/tests/testsuite/registry.rs index 163ab79a712..76528b29cd5 100644 --- a/tests/testsuite/registry.rs +++ b/tests/testsuite/registry.rs @@ -3276,8 +3276,7 @@ fn package_lock_inside_package_is_overwritten() { } #[cargo_test] -fn package_lock_as_a_symlink_inside_package_is_overwritten() { - let registry = registry::init(); +fn package_lock_as_a_symlink_inside_package_is_invalid() { let p = project() .file( "Cargo.toml", @@ -3300,21 +3299,23 @@ fn package_lock_as_a_symlink_inside_package_is_overwritten() { .symlink(".cargo-ok", "src/lib.rs") .publish(); - p.cargo("check").run(); + p.cargo("check") + .with_status(101) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] bar v0.0.1 (registry `dummy-registry`) +[ERROR] failed to download replaced source registry `crates-io` - let id = SourceId::for_registry(registry.index_url()).unwrap(); - let hash = cargo::util::hex::short_hash(&id); - let pkg_root = paths::cargo_home() - .join("registry") - .join("src") - .join(format!("-{}", hash)) - .join("bar-0.0.1"); - let ok = pkg_root.join(".cargo-ok"); - let librs = pkg_root.join("src/lib.rs"); +Caused by: + failed to unpack package `bar v0.0.1 (registry `dummy-registry`)` - // Is correctly overwritten and doesn't affect the file linked to - assert_eq!(ok.metadata().unwrap().len(), 7); - assert_eq!(fs::read_to_string(librs).unwrap(), "pub fn f() {}"); +Caused by: + invalid tarball downloaded, contains an entry at "bar-0.0.1/.cargo-ok" with invalid type Symlink + +"#]]) + .run(); } #[cargo_test] @@ -4770,13 +4771,7 @@ Caused by: failed to unpack package `bar v1.0.0 (registry `dummy-registry`)` Caused by: - failed to unpack entry at `bar-1.0.0/smuggled` - -Caused by: - failed to unpack `[ROOT]/home/.cargo/registry/src/-[HASH]/bar-1.0.0/smuggled` - -Caused by: - [..] when creating dir [ROOT]/home/.cargo/registry/src/-[HASH]/bar-1.0.0/smuggled + invalid tarball downloaded, contains an entry at "bar-1.0.0/smuggled" with invalid type Symlink "#]]) .run();