CVE-2018-20990
Description
An issue was discovered in the tar crate before 0.4.16 for Rust. Arbitrary file overwrite can occur via a symlink or hardlink in a TAR archive.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
The Rust tar crate before 0.4.16 allows arbitrary file overwrite via symlink or hardlink in a TAR archive.
Vulnerability
CVE-2018-20990 is a vulnerability in the Rust tar crate (versions before 0.4.16) where the unpack_in-family of functions do not properly validate symbolic and hard links inside a tar archive. This permits a malicious archive to specify links that point to files outside the intended extraction directory [1][2].
Exploitation
An attacker can exploit this by crafting a tar file containing symlinks or hardlinks that target arbitrary files on the filesystem. When a user or application extracts the archive using a vulnerable version of the crate, the link is followed and the target file is overwritten with the archive's content. No authentication or special privileges are required beyond the ability to trigger extraction [2].
Impact
Successful exploitation leads to arbitrary file overwrite, which can be leveraged for code execution (e.g., overwriting executables or libraries) or denial of service (e.g., overwriting critical system files). The CVSS score is 7.5 (High) with integrity impact high [2].
Mitigation
The issue is patched in tar crate version 0.4.16 and later. Users should update to at least this version to prevent exploitation [1][2]. There is no known workaround other than upgrading.
AI Insight generated on May 22, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
tarcrates.io | < 0.4.16 | 0.4.16 |
Affected products
2- Rust/tar cratedescription
Patches
154651a87ae6bMerge pull request #156 from alexcrichton/protect-hard-links
2 files changed · +129 −29
src/entry.rs+58 −29 modified@@ -4,7 +4,7 @@ use std::fs; use std::io::prelude::*; use std::io::{self, Error, ErrorKind, SeekFrom}; use std::marker; -use std::path::{Component, Path}; +use std::path::{Component, Path, PathBuf}; use filetime::{self, FileTime}; @@ -369,37 +369,14 @@ impl<'a> EntryFields<'a> { None => return Ok(false), }; - if !parent.exists() { + if parent.symlink_metadata().is_err() { fs::create_dir_all(&parent).map_err(|e| { TarError::new(&format!("failed to create `{}`", parent.display()), e) })?; } - // Abort if target (canonical) parent is outside of `dst` - let canon_parent = parent.canonicalize().map_err(|err| { - Error::new( - err.kind(), - format!("{} while canonicalizing {}", err, parent.display()), - ) - })?; - let canon_target = dst.canonicalize().map_err(|err| { - Error::new( - err.kind(), - format!("{} while canonicalizing {}", err, dst.display()), - ) - })?; - if !canon_parent.starts_with(&canon_target) { - let err = TarError::new( - &format!( - "trying to unpack outside of destination path: {}", - canon_target.display() - ), - // TODO: use ErrorKind::InvalidInput here? (minor breaking change) - Error::new(ErrorKind::Other, "Invalid argument"), - ); - return Err(err.into()); - } + let canon_target = self.validate_inside_dst(&dst, parent)?; self.unpack(Some(&canon_target), &file_dst).map_err(|e| { TarError::new(&format!("failed to unpack `{}`", file_dst.display()), e) @@ -441,8 +418,23 @@ impl<'a> EntryFields<'a> { return if kind.is_hard_link() { let link_src = match target_base { + // If we're unpacking within a directory then ensure that + // the destination of this hard link is both present and + // inside our own directory. This is needed because we want + // to make sure to not overwrite anything outside the root. + // + // Note that this logic is only needed for hard links + // currently. With symlinks the `validate_inside_dst` which + // happens before this method as part of `unpack_in` will + // use canonicalization to ensure this guarantee. For hard + // links though they're canonicalized to their existing path + // so we need to validate at this time. + Some(ref p) => { + let link_src = p.join(src); + self.validate_inside_dst(p, &link_src)?; + link_src + } None => src.into_owned(), - Some(ref p) => p.join(src), }; fs::hard_link(&link_src, dst).map_err(|err| Error::new( err.kind(), @@ -490,7 +482,16 @@ impl<'a> EntryFields<'a> { // As a result if we don't recognize the kind we just write out the file // as we would normally. - fs::File::create(dst).and_then(|mut f| { + // Remove an existing file, if any, to avoid writing through + // symlinks/hardlinks to weird locations. The tar archive says this is a + // regular file, so let's make it a regular file. + (|| -> io::Result<()> { + match fs::remove_file(dst) { + Ok(()) => {} + Err(ref e) if e.kind() == io::ErrorKind::NotFound => {} + Err(e) => return Err(e) + } + let mut f = fs::File::create(dst)?; for io in self.data.drain(..) { match io { EntryIo::Data(mut d) => { @@ -508,7 +509,7 @@ impl<'a> EntryFields<'a> { } } Ok(()) - }).map_err(|e| { + })().map_err(|e| { let header = self.header.path_bytes(); TarError::new(&format!("failed to unpack `{}` into `{}`", String::from_utf8_lossy(&header), @@ -596,6 +597,34 @@ impl<'a> EntryFields<'a> { Ok(()) } } + + fn validate_inside_dst(&self, dst: &Path, file_dst: &Path) -> io::Result<PathBuf> { + // Abort if target (canonical) parent is outside of `dst` + let canon_parent = file_dst.canonicalize().map_err(|err| { + Error::new( + err.kind(), + format!("{} while canonicalizing {}", err, file_dst.display()), + ) + })?; + let canon_target = dst.canonicalize().map_err(|err| { + Error::new( + err.kind(), + format!("{} while canonicalizing {}", err, dst.display()), + ) + })?; + if !canon_parent.starts_with(&canon_target) { + let err = TarError::new( + &format!( + "trying to unpack outside of destination path: {}", + canon_target.display() + ), + // TODO: use ErrorKind::InvalidInput here? (minor breaking change) + Error::new(ErrorKind::Other, "Invalid argument"), + ); + return Err(err.into()); + } + Ok(canon_target) + } } impl<'a> Read for EntryFields<'a> {
tests/entry.rs+71 −0 modified@@ -2,6 +2,7 @@ extern crate tar; extern crate tempdir; use std::fs::File; +use std::io::Read; use tempdir::TempDir; @@ -247,3 +248,73 @@ fn good_parent_paths_ok() { let dst = t!(td.path().join("foo").join("bar").canonicalize()); t!(File::open(dst)); } + +#[test] +fn modify_hard_link_just_created() { + let mut ar = tar::Builder::new(Vec::new()); + + let mut header = tar::Header::new_gnu(); + header.set_size(0); + header.set_entry_type(tar::EntryType::Link); + t!(header.set_path("foo")); + t!(header.set_link_name("../test")); + header.set_cksum(); + t!(ar.append(&header, &[][..])); + + let mut header = tar::Header::new_gnu(); + header.set_size(1); + header.set_entry_type(tar::EntryType::Regular); + t!(header.set_path("foo")); + header.set_cksum(); + t!(ar.append(&header, &b"x"[..])); + + let bytes = t!(ar.into_inner()); + let mut ar = tar::Archive::new(&bytes[..]); + + let td = t!(TempDir::new("tar")); + + let test = td.path().join("test"); + t!(File::create(&test)); + + let dir = td.path().join("dir"); + assert!(ar.unpack(&dir).is_err()); + + let mut contents = Vec::new(); + t!(t!(File::open(&test)).read_to_end(&mut contents)); + assert_eq!(contents.len(), 0); +} + +#[test] +fn modify_symlink_just_created() { + let mut ar = tar::Builder::new(Vec::new()); + + let mut header = tar::Header::new_gnu(); + header.set_size(0); + header.set_entry_type(tar::EntryType::Symlink); + t!(header.set_path("foo")); + t!(header.set_link_name("../test")); + header.set_cksum(); + t!(ar.append(&header, &[][..])); + + let mut header = tar::Header::new_gnu(); + header.set_size(1); + header.set_entry_type(tar::EntryType::Regular); + t!(header.set_path("foo")); + header.set_cksum(); + t!(ar.append(&header, &b"x"[..])); + + let bytes = t!(ar.into_inner()); + let mut ar = tar::Archive::new(&bytes[..]); + + let td = t!(TempDir::new("tar")); + + let test = td.path().join("test"); + t!(File::create(&test)); + + let dir = td.path().join("dir"); + t!(ar.unpack(&dir)); + + let mut contents = Vec::new(); + t!(t!(File::open(&test)).read_to_end(&mut contents)); + assert_eq!(contents.len(), 0); +}
Vulnerability mechanics
Root cause
"Insufficient validation of hard link targets and unsafe file creation allowed for arbitrary file overwrites via symlinks or hard links."
Attack vector
An attacker can craft a malicious TAR archive containing a hard link or symlink that points to a location outside the intended extraction directory. When the `tar` crate processes this archive, it may follow these links to overwrite arbitrary files on the filesystem with the contents of subsequent entries in the archive. This occurs because the crate previously lacked sufficient validation for hard link targets and did not safely handle existing files at the destination path [patch_id=16360].
Affected code
The vulnerability exists in `src/entry.rs` within the `EntryFields` implementation. Specifically, the logic for handling hard links and regular file creation failed to properly validate paths or remove existing files before writing, allowing for potential overwrites [patch_id=16360].
What the fix does
The patch introduces a `validate_inside_dst` helper function to ensure that all file operations remain within the intended extraction directory by canonicalizing paths and checking their prefixes [patch_id=16360]. For hard links, this validation is now explicitly performed before the link is created. Additionally, the code was updated to remove any existing file at the destination path before creating a new regular file, which prevents attackers from using pre-existing symlinks or hard links to redirect file writes to unauthorized locations [patch_id=16360].
Preconditions
- inputThe user must extract a malicious TAR archive using an affected version of the tar crate.
Generated on May 17, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-2367-c296-3mp2ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2018-20990ghsaADVISORY
- github.com/alexcrichton/tar-rs/commit/54651a87ae6ba7d81fcc72ffdee2ea7eca2c7e85ghsaWEB
- github.com/alexcrichton/tar-rs/pull/156ghsaWEB
- rustsec.org/advisories/RUSTSEC-2018-0002.htmlghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.