VYPR
Medium severity5.1NVD Advisory· Published Mar 20, 2026· Updated Mar 20, 2026

tar-rs incorrectly ignores PAX size headers if header size is nonzero

CVE-2026-33055

Description

tar-rs is a tar archive reading/writing library for Rust. Versions 0.4.44 and below have conditional logic that skips the PAX size header in cases where the base header size is nonzero. As part of CVE-2025-62518, the astral-tokio-tar project was changed to correctly honor PAX size headers in the case where it was different from the base header. This is almost the inverse of the astral-tokio-tar issue. Any discrepancy in how tar parsers honor file size can be used to create archives that appear differently when unpacked by different archivers. In this case, the tar-rs (Rust tar) crate is an outlier in checking for the header size - other tar parsers (including e.g. Go archive/tar) unconditionally use the PAX size override. This can affect anything that uses the tar crate to parse archives and expects to have a consistent view with other parsers. This issue has been fixed in version 0.4.45.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

tar-rs crate versions ≤0.4.44 ignore PAX size headers when the base header size is nonzero, enabling archive differentials that can hide or expose files across parsers.

Vulnerability

The Rust tar crate (tar-rs) versions 0.4.44 and below contain a conditional logic flaw in PAX extended header handling. When a tar entry has both a base header size field and a PAX extended header that overrides the size, the crate incorrectly skips the PAX size field is nonzero. This is the inverse of the issue addressed in CVE-2025-62518 for the astral-tokio-tar project [1][3]. Most other tar parsers, including Go's archive/tar, unconditionally honor the PAX size override [1].

Exploitation

An attacker can craft a malicious tar archive where a PAX extended header declares a larger file size than the base header. The inflated region can contain hidden entries (e.g., symlinks or other files) that are skipped by tar-rs but would be extracted by other parsers that correctly honor the PAX size [2]. The attack requires no authentication and can be delivered over the network, though user interaction (e.g., extracting an archive) is needed [4].

Impact

This discrepancy creates a "tar differential" that can be used to smuggle content past tar-rs-based scanners or validators while remaining visible to other tools. For example, a symlink hidden in the inflated region would be ignored by tar-rs but extracted by a compliant parser, potentially leading to file overwrites or path traversal [2][3]. The CVSS score is 5.1 (Medium) with low confidentiality and integrity impacts [4].

Mitigation

The issue is fixed in tar-rs version 0.4.45, which unconditionally honors the PAX size header [1][2]. Users should update to this version or later. No workarounds are documented; the crate is widely used in the Rust ecosystem, including indirectly through tools like Cargo (though crates.io rejects symlinks and hard links, limiting direct exploitation there) [3].

AI Insight generated on May 18, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2
  • tar-rs/tar-rsllm-create
    Range: <=0.4.44
  • alexcrichton/tar-rsv5
    Range: < 0.4.45

Patches

1
de1a5870e603

archive: Unconditionally honor PAX size (#441)

https://github.com/alexcrichton/tar-rsAlex CrichtonMar 19, 2026via ghsa
3 files changed · +159 4
  • Cargo.toml+3 0 modified
    @@ -23,8 +23,11 @@ contents are never required to be entirely resident in memory all at once.
     filetime = "0.2.8"
     
     [dev-dependencies]
    +astral-tokio-tar = "0.5"
     rand = { version = "0.8", features = ["small_rng"] }
     tempfile = "3"
    +tokio = { version = "1", features = ["macros", "rt"] }
    +tokio-stream = "0.1"
     
     [target."cfg(unix)".dependencies]
     xattr = { version = "1.1.3", optional = true }
    
  • src/archive.rs+5 4 modified
    @@ -337,10 +337,11 @@ impl<'a> EntriesFields<'a> {
     
             let file_pos = self.next;
             let mut size = header.entry_size()?;
    -        if size == 0 {
    -            if let Some(pax_size) = pax_size {
    -                size = pax_size;
    -            }
    +        // If this exists, it must override the header size. Disagreement among
    +        // parsers allows construction of malicious archives that appear different
    +        // when parsed.
    +        if let Some(pax_size) = pax_size {
    +            size = pax_size;
             }
             let ret = EntryFields {
                 size,
    
  • tests/all.rs+151 0 modified
    @@ -1871,3 +1871,154 @@ fn append_data_error_does_not_corrupt_subsequent_entries() {
         assert_eq!(entries.len(), 1);
         assert_eq!(entries[0].path().unwrap().to_str().unwrap(), "clean.txt");
     }
    +
    +/// Build the PAX size smuggling archive described in the original report.
    +///
    +/// A PAX extended header declares `size=2048` for a regular file whose
    +/// actual header size field is 8. A symlink entry is hidden inside the
    +/// inflated region. A correct parser honours the PAX size and skips over
    +/// the symlink; a buggy one reads only the header size and exposes it.
    +fn build_pax_smuggle_archive() -> Vec<u8> {
    +    const B: usize = 512;
    +    const INFLATED: usize = 2048;
    +    let end_of_archive = || std::iter::repeat(0u8).take(B * 2);
    +
    +    let mut ar: Vec<u8> = Vec::new();
    +
    +    // PAX extended header declaring size=2048 for the next entry.
    +    let pax_rec = format!("13 size={INFLATED}\n");
    +    let mut pax_hdr = Header::new_ustar();
    +    pax_hdr.set_path("./PaxHeaders/regular").unwrap();
    +    pax_hdr.set_size(pax_rec.as_bytes().len() as u64);
    +    pax_hdr.set_entry_type(EntryType::XHeader);
    +    pax_hdr.set_cksum();
    +    ar.extend_from_slice(pax_hdr.as_bytes());
    +    ar.extend_from_slice(pax_rec.as_bytes());
    +    ar.resize(ar.len().next_multiple_of(B), 0);
    +
    +    // Regular file whose header says size=8, but PAX says 2048.
    +    let content = b"regular\n";
    +    let mut file_hdr = Header::new_ustar();
    +    file_hdr.set_path("regular.txt").unwrap();
    +    file_hdr.set_size(content.len() as u64);
    +    file_hdr.set_entry_type(EntryType::Regular);
    +    file_hdr.set_cksum();
    +    ar.extend_from_slice(file_hdr.as_bytes());
    +    let mark = ar.len();
    +    ar.extend_from_slice(content);
    +    ar.resize(ar.len().next_multiple_of(B), 0);
    +
    +    // Smuggled symlink hidden in the inflated region.
    +    let mut sym_hdr = Header::new_ustar();
    +    sym_hdr.set_path("smuggled").unwrap();
    +    sym_hdr.set_size(0);
    +    sym_hdr.set_entry_type(EntryType::Symlink);
    +    sym_hdr.set_link_name("/etc/shadow").unwrap();
    +    sym_hdr.set_cksum();
    +    ar.extend_from_slice(sym_hdr.as_bytes());
    +    ar.extend(end_of_archive());
    +
    +    // Pad to fill the inflated window.
    +    let used = ar.len() - mark;
    +    let pad = INFLATED.saturating_sub(used);
    +    ar.extend(std::iter::repeat(0u8).take(pad.next_multiple_of(B)));
    +
    +    // End-of-archive.
    +    ar.extend(end_of_archive());
    +    ar
    +}
    +
    +/// Regression test for PAX size smuggling.
    +///
    +/// A crafted archive uses a PAX extended header to declare a file size (2048)
    +/// larger than the header's octal size field (8). Before the fix, `tar-rs`
    +/// only applied the PAX size override when the header size was 0, so it would
    +/// read the small header size, advance too little, and expose a symlink entry
    +/// hidden in the "padding" area. After the fix, the PAX size unconditionally
    +/// overrides the header size, causing the parser to skip over the smuggled
    +/// symlink — matching the behavior of compliant parsers.
    +#[test]
    +fn pax_size_smuggled_symlink() {
    +    let data = build_pax_smuggle_archive();
    +
    +    let mut archive = Archive::new(random_cursor_reader(&data[..]));
    +    let entries: Vec<_> = archive
    +        .entries()
    +        .unwrap()
    +        .map(|e| {
    +            let e = e.unwrap();
    +            let path = e.path().unwrap().to_path_buf();
    +            let kind = e.header().entry_type();
    +            let link = e.link_name().unwrap().map(|l| l.to_path_buf());
    +            (path, kind, link)
    +        })
    +        .collect();
    +
    +    // With the fix applied, only "regular.txt" should be visible.
    +    // The smuggled symlink must NOT appear.
    +    let expected: Vec<(PathBuf, EntryType, Option<PathBuf>)> =
    +        vec![(PathBuf::from("regular.txt"), EntryType::Regular, None)];
    +    assert_eq!(
    +        entries, expected,
    +        "smuggled symlink visible or unexpected entries\n\
    +         got: {entries:?}"
    +    );
    +}
    +
    +/// Cross-validate that `tar` and `astral-tokio-tar` parse the PAX size
    +/// smuggling archive identically, guarding against parsing differentials.
    +#[tokio::test]
    +async fn pax_size_smuggle_matches_astral_tokio_tar() {
    +    use tokio_stream::StreamExt;
    +
    +    let data = build_pax_smuggle_archive();
    +
    +    // Parse with sync tar.
    +    let sync_entries: Vec<_> = {
    +        let mut ar = Archive::new(&data[..]);
    +        ar.entries()
    +            .unwrap()
    +            .map(|e| {
    +                let e = e.unwrap();
    +                let path = e.path().unwrap().to_path_buf();
    +                let kind = e.header().entry_type();
    +                let link = e.link_name().unwrap().map(|l| l.to_path_buf());
    +                (path, kind, link)
    +            })
    +            .collect()
    +    };
    +
    +    // Parse with async astral-tokio-tar.
    +    let async_entries: Vec<_> = {
    +        let mut ar = tokio_tar::Archive::new(&data[..]);
    +        let mut entries = ar.entries().unwrap();
    +        let mut result = Vec::new();
    +        while let Some(e) = entries.next().await {
    +            let e = e.unwrap();
    +            let entry_type = e.header().entry_type();
    +            result.push((
    +                e.path().unwrap().to_path_buf(),
    +                // Map through the raw byte so the two crates' EntryTypes compare.
    +                EntryType::new(entry_type.as_byte()),
    +                e.link_name().unwrap().map(|l| l.to_path_buf()),
    +            ));
    +        }
    +        result
    +    };
    +
    +    // Assert exact expected content for both parsers independently,
    +    // so we verify correctness — not just mutual agreement.
    +    let expected: Vec<(PathBuf, EntryType, Option<PathBuf>)> =
    +        vec![(PathBuf::from("regular.txt"), EntryType::Regular, None)];
    +
    +    assert_eq!(
    +        sync_entries, expected,
    +        "tar-rs produced unexpected entries (smuggled symlink visible?)\n\
    +         got: {sync_entries:?}"
    +    );
    +    assert_eq!(
    +        async_entries, expected,
    +        "astral-tokio-tar produced unexpected entries (smuggled symlink visible?)\n\
    +         got: {async_entries:?}"
    +    );
    +}
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.