TL;DR
- CVE-2026-5223 — Cargo extracted symlinks inside crate tarballs without bounding the link target, so a crafted tarball from a third-party registry could place files one directory above the crate's cache directory and overwrite the cached source of another crate from the same registry.
- All Cargo versions shipped before Rust 1.96.0 are affected. The fix lands in Rust 1.96.0 on 2026-05-28 and rejects any symlink inside a crate tarball, regardless of source.
- crates.io users are not exposed: crates.io has always rejected uploads containing symlinks. The CVE is specifically a third-party-registry hole.
- Reported by Christos Papakonstantinou under the Rust security policy.
What the bug actually does
Cargo unpacks downloaded crate tarballs into a per-registry cache, one directory per crate version. The advisory's wording:
It was discovered that it's possible to craft a malicious tarball able to extract files one level below the crate's own cache directory.
"One level below" reads as one level up in filesystem terms — the symlink target escapes the crate's own cache directory into the sibling crates' directories that share the registry root. The blast is bounded to crates from the same registry the bad tarball lives in, because Cargo keeps registry caches isolated.
There is no execution required at unpack time. The poisoned cache sits there until the next cargo build resolves a victim crate, at which point the developer compiles attacker-controlled source under the name of a crate they thought they had pinned.
Why crates.io is the exception, not the rule
Cargo's defense for the official registry was at upload time: crates.io rejects any tarball that contains a symlink. So the vulnerability is invisible to anyone whose Cargo.toml only references crates.io. Third-party registries — internal company mirrors, vendor-hosted registries, alternative public registries — were never required to enforce the same rule, and Cargo trusted the tarball.
The 1.96.0 fix runs the symlink-reject on the client. crates.io users get the rejection too, even though their upload-time check would have caught the same construct upstream — extra coverage at zero behavioral cost, because crates.io never accepted symlinked tarballs in the first place.
What to do
- Pin to Rust 1.96.0 on or after 2026-05-28 wherever you build crates pulled from anything other than crates.io. That is the only step that closes the class.
- If you cannot move toolchains immediately, the advisory's interim guidance is to audit your registry contents for symlinks and configure the registry to reject symlinked tarballs if it offers the option. Most self-hosted registry implementations expose an upload-time hook; some do not, and those need the toolchain bump.
- Treat already-cached crates from third-party registries as potentially poisoned if you cannot rule out a malicious upload window. Clearing
~/.cargo/registry/src/<registry>/and re-fetching against a known-good registry snapshot is the cheap way to reset state before upgrading. - crates.io-only shops can still upgrade as a defense-in-depth move; there is no behavioral regression for tarballs the upload-time check already accepted.
Where this fits
Tarball-extraction escapes are an old class. Zip-slip (Snyk, 2018) named the same shape across Java, JavaScript, Python, Ruby, and Go archive libraries; the Cargo bug is the symlink variant, with the per-registry cache layout deciding where the escape lands. The client-side reject is the posture that survives a hostile or compromised registry; an upload-time check at one registry does not.
The Rust team shipped the advisory, the fixed-version commitment, and the mitigation guidance the same day, three days ahead of the 1.96.0 stable cut. Reporter credit goes to Christos Papakonstantinou under the Rust security policy.
Sources
- CVE-2026-5223: Cargo does not check symlinks when packaging (Rust Security Response WG advisory, 2026-05-25 — mechanism, affected versions, fix release date, mitigation guidance, reporter credit)
- Zip Slip Vulnerability (Snyk research, 2018 — original archive-extraction-escape disclosure across multiple language ecosystems; named precedent for the class)