GHSA-35rm-7j9c-2f7m async-tar PAX extension-header desync enables tar entry/content smuggling
Summary`async-tar` v0.6.0 mis-applies a buffered PAX `size` extension to an intermediary
extension header (a GNU longname `L`, a GNU longlink `K`, or a PAX `x`/`g`
header) instead of to the next *file* entry. POSIX requires a PAX extended-header
record set to describe the next file entry, never an intervening extension
header. Because `poll_next_raw` (`src/archive.rs`) threads the buffered PAX
records into the size computation of whatever raw header it reads next — and that
header can be an intermediary `L` — the stream cursor is advanced by an
attacker-chosen amount when the `L` body is consumed. The parser then desyncs
relative to a POSIX-correct tar parser (e.g. GNU tar), reading subsequent bytes
at the wrong block boundary.An attacker who can influence a tar stream that an `async-tar` consumer extracts
can construct an `x → L → file` sequence whose entry list and on-disk result
differ between `async-tar` and a reference parser. This enables content/entry
smuggling: a file that a GNU-tar-based scanner/validator/AV sees as benign opaque
data is extracted by `async-tar` as a different file with different bytes (e.g. an
executable script), and vice versa.Type confusion / improper validation of the specified quantity (size). CWE-20,
CWE-843. Severity assessed Medium, consistent with the same defect class in the
upstream tar-rs / tokio-tar lineage.Affected codePackage: `async-tar` (crates.io). Affected version: **0.6.0** (latest release) and
current `main` HEAD. Both lack the extension-header guard.`src/archive.rs`, `poll_next_raw` (line numbers from the v0.6.0 tag,
commit `45814b19295b7398e119c90c57d8c8bf70a798b6`):and a few lines further down the same function:The caller loop in `src/archive.rs` (`Entries::poll_next`) buffers a PAX local
extension into `current_pax_extensions` and then calls `poll_next_raw` with
`current_pax_extensions.as_deref()` for the *next* raw header. When that next
raw header is an intermediary GNU longname (handled by the `is_gnu_longname()`
branch a few lines later), the PAX `size` is applied to it, so `*next` advances by
the spoofed size rather than the `L` header's own declared size. That is the
desync.The buffered PAX records are intended to apply only to the following *file*
entry; the missing check is whether the raw header currently being sized is itself
an extension header (`L`/`K`/`x`/`g`).ImpactDifferential extraction / entry smuggling. A consumer that extracts an
attacker-influenced tar stream with `async-tar` (e.g. a server endpoint that
unpacks an uploaded `.tar`/`.tar.gz`, a dependency/artifact fetcher that unpacks
a remote tarball, an archive-preview/scan pipeline) will:materialize files / file contents that a POSIX-correct parser (GNU tar,
libarchive/bsdtar) does not surface, andomit or alter files that the reference parser does surface.This breaks any security control that relies on scanning the archive with one
parser and extracting with `async-tar`: a malware/secret scanner reading the
stream with GNU tar can be made to see only benign data while `async-tar` writes
an executable payload to disk. It can also be used to hide entries from
audit/inventory tooling, or to write content to a path the reviewer believes
holds something else. No attacker-controlled local state is required — only the
ability to influence the bytes of the tar stream that the consumer extracts.How input reaches the sink (reachability)The vulnerable path is the library's primary public API for reading archives:
`Archive::new(reader).entries()` returns an `Entries` stream whose `poll_next`
drives `poll_next_raw` for every header. Any consumer that iterates entries (or
calls `unpack`/`unpack_in` on them) of an attacker-influenced tar stream reaches
the sink with no additional configuration. The `reader` need not be a file — it is
any `AsyncRead`, so an upload buffer, an HTTP response body, or a decompressor
output all qualify. The only precondition for the desync is that the stream
contain a PAX local-extension header (`x`) carrying a `size` record immediately
followed by an intermediary GNU longname (`L`) before the next file header — a
structure the attacker fully controls in the archive bytes. Representative
reachable consumers are server endpoints that unpack uploaded `.tar`/`.tar.gz`
bodies, dependency/artifact fetchers that unpack remote tarballs, and
archive-scan/preview pipelines.Proof of conceptA standalone Rust consumer binary that links the published crates.io
`async-tar = "=0.6.0"` (`default-features = false, features = ["runtime-tokio"]`)
and runs the real `Archive::new(...).entries()` extraction loop (the same shape
used by real downstream server consumers that unpack uploaded tarballs). It reads
a tar file and writes each entry to a destination directory, printing the entry
list `async-tar` surfaces. A second binary hand-crafts the malicious and benign
tar byte streams.Malicious archive geometry (block = 512 bytes):GNU tar honours the `L` header's own declared size (1 block) for the longname and
ignores the buffered PAX `size`, so it reads B3 as the longname, treats B4 as the
file, and reads B5 as that file's opaque data. `async-tar` mis-applies the PAX
`size` (2 blocks) to the `L` header, reads B3+B4 as the longname, lands its cursor
on B5, parses it as a tar header, and extracts the smuggled `hidden_payload.sh`
body (B6).Tar-builder source (`mktar.rs`):Consumer source (`main.rs`, mirrors a real `Archive::entries()` extraction loop):`Cargo.toml`:End-to-end reproductionReference parser: GNU tar 1.35. async-tar: the v0.6.0 crates.io release linked by
the consumer binary above. Verbatim captured output:Same bytes, two parsers, different on-disk result: GNU tar writes a 1024-byte
benign blob; `async-tar` writes a 65-byte executable shell script that the
reference parser never exposes as an entry. The smuggled `#!/bin/sh` body is
content a GNU-tar-based scanner would never inspect.Negative control — a benign archive (correct PAX usage: `x` applies `path` to the
following file, no intermediary `L`):GNU tar and `async-tar` produce identical entry lists and identical on-disk files.
No smuggling. The differential is exclusive to the `x → L → file` desync sequence.FixApply the buffered PAX records (and the `size`/`uid`/`gid` overrides) only when
the raw header being sized is NOT itself an extension header. Skip the override
for GNU longname (`L`), GNU longlink (`K`), and PAX local/global (`x`/`g`) headers,
whose body length must come from their own declared size. This mirrors the fix
adopted in the upstream tar-rs / tokio-tar lineage for the same defect class.Fix-verify, captured verbatim. The patched `async-tar` (guard added) re-run
against the same `mal.tar`:With the guard, `async-tar`'s view converges with GNU tar's: it surfaces
`GNU_SEES_THIS.txt` with the opaque B5 bytes (`hidden_payload.sh...`) as data, and
no longer extracts the smuggled executable script. The benign control still
produces the correct two-file output. The desync is eliminated.Fix PRA fix PR adding the `is_extension_header` guard to `poll_next_raw` in
`src/archive.rs` is opened from the advisory's temporary private fork against this
repository. It carries the diff shown in the **Fix** section above (no behavioural
change for well-formed archives; only intermediary `L`/`K`/`x`/`g` headers stop
inheriting a following PAX `size`).CreditReported by tonghuaroot.
CVSS v4.0 score: 6.3