Package manager security discussions tend to focus on what happens after code runs: malicious build scripts, compromised dependencies calling out to external servers, supply chain attacks embedded in logic. CVE-2026-33056 is a reminder that the attack surface starts earlier, at the moment Cargo unpacks an archive before any code compiles at all.
The Rust Security Response Team’s advisory describes a vulnerability in the tar crate, a pure-Rust library that Cargo depends on to extract .crate files during a build. A maliciously crafted archive could change permissions on arbitrary directories on the filesystem during extraction. The fix is in Rust 1.94.1, released March 26th, 2026.
What .crate Files Actually Are
When Cargo fetches a dependency, it downloads a .crate file from the registry. These are gzipped tar archives where all entries are namespaced under a {name}-{version}/ prefix. Cargo stores the compressed archive in ~/.cargo/registry/cache/ and extracts the contents to ~/.cargo/registry/src/.
The extraction step looks simple from the outside: decompress the archive, call unpack_in() targeting the source directory, proceed to compilation. The unpack_in() method on the tar crate is designed to contain all extracted files within the target directory, rejecting entries whose paths would escape it.
The problem is that rejecting path traversal for file writes is a separate concern from preventing permission manipulation on existing paths. Those two operations go through different code paths in the extractor, and that separation is where this vulnerability lives.
Permission Setting as a Distinct Operation
Every entry in a tar archive carries metadata beyond file data. Each 512-byte header block encodes the file mode (Unix permissions), owner UID and GID, timestamps, file type (regular file, directory, symlink, hard link), and for symlinks, the link target. When an extractor processes a directory entry, the typical sequence is:
- Resolve the entry’s path relative to the extraction target
- Create the directory if it does not already exist
- Call
set_permissions()on the resolved path to apply the mode from the archive header
Safe extraction libraries invest their defenses in step one: strip .. path components, reject absolute paths, refuse entries that would escape the target. But consider what happens when an archive contains a symlink entry followed by a directory entry whose path routes through that symlink:
Entry A (symlink): "build-output" -> "/home/user/.cargo"
Entry B (directory): "build-output/subdir" with mode 0000
If the extractor follows symlinks when resolving paths for permission operations, entry B causes set_permissions(0000) to be called on /home/user/.cargo/subdir, or depending on the resolution semantics, on /home/user/.cargo itself. No file was written outside the target directory. The path-traversal guard said nothing about this case because it was designed to prevent file creation outside the target, not to gate every filesystem operation that touches a resolved path.
An attacker who can craft an archive could make important directories unreadable, world-writable, or otherwise compromised before a single line of build script executes. On a developer machine, ~/.cargo or ~/.ssh are plausible targets. The impact varies by what the attacker wants: a DoS against the build system, a privilege escalation setup, or groundwork for a more targeted follow-on attack.
This Vulnerability Class Has a Long History
Archive extraction has been a consistent source of security failures across ecosystems, and the failures follow a recognizable pattern: libraries implement defenses for one category of exploit while leaving adjacent categories unaddressed.
The most prolonged example is Python’s tarfile module. Path traversal via ../ components in archive entry names was documented in CVE-2007-4559 in 2007. The Python standard library did not ship a proper fix until Python 3.12, fifteen years later, through tarfile.data_filter. The delay was partly because the dangerous behavior was framed as user responsibility in the documentation, and partly because the exploit path seemed narrow in practice. The Python community eventually reclassified the severity and backported the filter option.
The zip slip vulnerability class, catalogued by Snyk in 2018, found essentially the same path traversal issue in archive libraries across Java, .NET, Go, Python, and Ruby. Most implementations had validated paths for some operations and left others unguarded. The pattern repeated because archive library authors reasoned about the file-writing case and did not consider every downstream operation on the resolved path.
The Cargo case is a variant: the archive does not write files outside the target directory, yet chmod operations still reach paths beyond it. The extractor handled writes correctly and applied insufficient validation to permission operations.
The Response Timeline
The Rust team’s handling of this disclosure is worth examining in detail. Sergei Zimmerman discovered the underlying vulnerability in the tar crate and notified the Rust project before any public disclosure. William Woodruff directly assisted the crates.io team with mitigations.
From there, the response proceeded in a specific order:
- March 13th: crates.io deployed validation to reject uploads of archives that exploit this vulnerability
- The team audited every crate ever published to crates.io; none were found to be exploiting the vulnerability
- March 21st: the public advisory was published, disclosing CVE-2026-33056
- March 26th: Rust 1.94.1 released, patching the
tarcrate
The sequence is deliberate. Deploying the registry-level rejection first meant that by the time the advisory was public, the crates.io attack vector was already closed for new uploads. Auditing all existing published crates before the public disclosure meant the advisory could state clearly that no published crate exploits this. Users evaluating their own risk got a concrete, verifiable answer rather than an uncertain waiting period.
This is a meaningful contrast to disclosures where the advisory precedes mitigations, leaving users to wonder whether packages already in the registry are weaponized.
The Alternate Registry Gap
The advisory is explicit about the boundary of these mitigations: users of alternate registries are not covered by what crates.io deployed on March 13th.
Private Cargo registries, whether hosted through Cloudsmith, JFrog Artifactory, or a self-hosted implementation using the Cargo registry protocol, operate independently of crates.io’s upload validation. A malicious or compromised package on such a registry would face no equivalent server-side check.
The Rust toolchain fix in 1.94.1 does close the extraction-level vulnerability regardless of which registry the package came from. But the advisory is careful to note that older versions of Cargo, running against alternate registries that have not deployed equivalent mitigations, remain exposed.
For teams with internal Cargo registries the practical steps are:
- Upgrade to Rust 1.94.1 to get the patched
tarcrate at the toolchain level - Contact your registry vendor to ask whether server-side validation analogous to crates.io’s March 13th deployment has been added
- Audit packages fetched from alternate registries before the 1.94.1 upgrade date, particularly any that were added or updated recently
If your project uses the tar crate directly, you can check Cargo.lock to confirm the version in use after upgrading:
[[package]]
name = "tar"
version = "0.4.44" # verify this matches the patched version shipped with 1.94.1
source = "registry+https://github.com/rust-lang/crates.io-index"
You can also set an explicit minimum version in Cargo.toml to prevent older versions from being resolved:
[dependencies]
tar = { version = ">=0.4.44" } # replace with the confirmed patched version
What the Patch Actually Does
The fix in the tar crate extends path normalization to cover permission-setting operations, applying the same boundary checks that unpack_in() uses for file creation to any call that resolves a path before invoking set_permissions(). Symlink traversal during path resolution for chmod operations is now subject to the same containment guarantee as file writes.
The relevant public API surface is Archive::unpack() and Entry::unpack_in(). Before the fix, the containment guarantee of unpack_in() covered where files were written; after the fix, it covers where permissions are applied.
What This Tells Us About Build System Trust
Build systems occupy a privileged position on developer machines. They download code from external sources, unpack it, and operate on the filesystem with the full privileges of the running user. Cargo’s security model does substantial work here: checksum verification against the registry index, reproducible resolution via Cargo.lock, a registry with upload moderation and audit capabilities. CVE-2026-33056 shows that the unpacking step itself carries risk that is distinct from and prior to the code execution risk.
“Safe extraction” is not a binary property. It means containing file writes to the target directory, containing permission operations to the target directory, not following symlinks in ways that escape the sandbox, and handling each of those guarantees consistently across all the operations an extractor can perform. The history of this vulnerability class suggests that most extractor implementations have achieved some subset of those guarantees and left the rest as assumptions.