VYPR
High severity8.1OSV Advisory· Published Oct 21, 2025· Updated Apr 15, 2026

CVE-2025-62518

CVE-2025-62518

Description

astral-tokio-tar is a tar archive reading/writing library for async Rust. Versions of astral-tokio-tar prior to 0.5.6 contain a boundary parsing vulnerability that allows attackers to smuggle additional archive entries by exploiting inconsistent PAX/ustar header handling. When processing archives with PAX-extended headers containing size overrides, the parser incorrectly advances stream position based on ustar header size (often zero) instead of the PAX-specified size, causing it to interpret file content as legitimate tar headers. This issue has been patched in version 0.5.6. There are no workarounds.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
astral-tokio-tarcrates.io
< 0.5.60.5.6
tokio-tarcrates.io
<= 0.3.1

Affected products

1

Patches

1
22b3f884adb7

Merge commit from fork

https://github.com/astral-sh/tokio-tarWilliam WoodruffOct 21, 2025via ghsa
3 files changed · +91 11
  • src/archive.rs+74 11 modified
    @@ -15,12 +15,12 @@ use tokio::{
     };
     use tokio_stream::*;
     
    -use crate::header::BLOCK_SIZE;
     use crate::{
         entry::{EntryFields, EntryIo},
         error::TarError,
         other, Entry, GnuExtSparseHeader, GnuSparseHeader, Header,
     };
    +use crate::{header::BLOCK_SIZE, pax::pax_extensions};
     
     /// A top-level representation of an archive file.
     ///
    @@ -202,7 +202,7 @@ impl<R: Read + Unpin> Archive<R> {
             Ok(Entries {
                 archive: self.clone(),
                 pending: None,
    -            current: (0, None, 0, None),
    +            current: (0, None, 0, None, None),
                 gnu_longlink: (false, None),
                 gnu_longname: (false, None),
                 pax_extensions: (false, None),
    @@ -305,7 +305,13 @@ impl<R: Read + Unpin> Archive<R> {
     /// Stream of `Entry`s.
     pub struct Entries<R: Read + Unpin> {
         archive: Archive<R>,
    -    current: (u64, Option<Header>, usize, Option<GnuExtSparseHeader>),
    +    current: (
    +        u64,
    +        Option<Header>,
    +        usize,
    +        Option<GnuExtSparseHeader>,
    +        Option<Vec<u8>>,
    +    ),
         /// The [`Entry`] that is currently being processed.
         pending: Option<Entry<Archive<R>>>,
         /// GNU long name extension.
    @@ -357,13 +363,15 @@ impl<R: Read + Unpin> Stream for Entries<R> {
                 let entry = if let Some(entry) = self.pending.take() {
                     entry
                 } else {
    -                let (next, current_header, current_header_pos, _) = &mut self.current;
    +                let (next, current_header, current_header_pos, _, pax_extensions) =
    +                    &mut self.current;
                     ready_opt_err!(poll_next_raw(
                         archive,
                         next,
                         current_header,
                         current_header_pos,
    -                    cx
    +                    cx,
    +                    pax_extensions.as_deref(),
                     ))
                 };
     
    @@ -445,6 +453,7 @@ impl<R: Read + Unpin> Stream for Entries<R> {
                     }
     
                     self.pax_extensions.0 = true;
    +                self.current.4 = self.pax_extensions.1.clone();
                     continue;
                 }
     
    @@ -463,15 +472,15 @@ impl<R: Read + Unpin> Stream for Entries<R> {
                 }
     
                 let archive = self.archive.clone();
    -            let (next, _, current_pos, current_ext) = &mut self.current;
    +            let (next, _, current_pos, current_ext, _pax_extensions) = &mut self.current;
     
                 ready_err!(poll_parse_sparse_header(
                     archive,
                     next,
                     current_ext,
                     current_pos,
                     &mut fields,
    -                cx
    +                cx,
                 ));
     
                 return Poll::Ready(Some(Ok(fields.into_entry())));
    @@ -491,7 +500,7 @@ impl<R: Read + Unpin> Stream for RawEntries<R> {
         fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
             let archive = self.archive.clone();
             let (next, current_header, current_header_pos) = &mut self.current;
    -        poll_next_raw(archive, next, current_header, current_header_pos, cx)
    +        poll_next_raw(archive, next, current_header, current_header_pos, cx, None)
         }
     }
     
    @@ -501,6 +510,7 @@ fn poll_next_raw<R: Read + Unpin>(
         current_header: &mut Option<Header>,
         current_header_pos: &mut usize,
         cx: &mut Context<'_>,
    +    pax_extensions_data: Option<&[u8]>,
     ) -> Poll<Option<io::Result<Entry<Archive<R>>>>> {
         let mut header_pos = *next;
     
    @@ -561,13 +571,66 @@ fn poll_next_raw<R: Read + Unpin>(
         }
     
         let file_pos = *next;
    -    let size = header.entry_size()?;
    +
    +    let mut header = current_header.take().unwrap();
    +
    +    // note when pax extensions are available, the size from the header will be ignored
    +    let mut size = header.entry_size()?;
    +
    +    // the size above will be overriden by the pax data if it has a size field.
    +    // same for uid and gid, which will be overridden in the header itself.
    +    if let Some(pax) = pax_extensions_data.map(pax_extensions) {
    +        for extension in pax {
    +            let extension = extension?;
    +
    +            // ignore keys that aren't parsable as a string at this stage.
    +            // that isn't relevant to the size/uid/gid processing.
    +            let Ok(key) = extension.key() else {
    +                continue;
    +            };
    +
    +            match key {
    +                "size" => {
    +                    let size_str = extension
    +                        .value()
    +                        .map_err(|_e| other("failed to parse pax size as string"))?;
    +                    size = size_str
    +                        .parse::<u64>()
    +                        .map_err(|_e| other("failed to parse pax size"))?;
    +                }
    +
    +                "uid" => {
    +                    let uid_str = extension
    +                        .value()
    +                        .map_err(|_e| other("failed to parse pax uid as string"))?;
    +                    header.set_uid(
    +                        uid_str
    +                            .parse::<u64>()
    +                            .map_err(|_e| other("failed to parse pax uid"))?,
    +                    );
    +                }
    +
    +                "gid" => {
    +                    let gid_str = extension
    +                        .value()
    +                        .map_err(|_e| other("failed to parse pax gid as string"))?;
    +                    header.set_gid(
    +                        gid_str
    +                            .parse::<u64>()
    +                            .map_err(|_e| other("failed to parse pax gid"))?,
    +                    );
    +                }
    +
    +                _ => {
    +                    continue;
    +                }
    +            }
    +        }
    +    }
     
         let mut data = VecDeque::with_capacity(1);
         data.push_back(EntryIo::Data(archive.clone().take(size)));
     
    -    let header = current_header.take().unwrap();
    -
         let ret = EntryFields {
             size,
             header_pos,
    
  • tests/all.rs+17 0 modified
    @@ -976,6 +976,23 @@ async fn pax_path() {
         assert!(first.path().unwrap().ends_with("aaaaaaaaaaaaaaa"));
     }
     
    +#[tokio::test]
    +async fn pax_precedence() {
    +    let mut ar = Archive::new(tar!("pax-header-precedence.tar"));
    +    let mut entries = t!(ar.entries());
    +
    +    let first = t!(entries.next().await.unwrap());
    +    assert!(first.path().unwrap().ends_with("normal.txt"));
    +
    +    let second = t!(entries.next().await.unwrap());
    +    assert!(second.path().unwrap().ends_with("blob.bin"));
    +
    +    let third = t!(entries.next().await.unwrap());
    +    assert!(third.path().unwrap().ends_with("marker.txt"));
    +
    +    assert!(entries.next().await.is_none());
    +}
    +
     #[tokio::test]
     async fn long_name_trailing_nul() {
         let mut b = Builder::new(Vec::<u8>::new());
    
  • tests/archives/pax-header-precedence.tar+0 0 added

Vulnerability mechanics

Generated by null/stub 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.