Tech7 min read

Rust zip Crate CVE-2026-115905: Header Consistency Disabled by Default in 9.0.5

IkesanContents

TL;DR

Impact When inspecting archives via the Central Directory with ZipArchive but extracting via Local File Headers with ZipStreamReader, mismatched files (such as executable scripts masquerading as harmless PDFs during inspection) can be written to disk. Remote code execution (RCE) can occur if extracted scripts are executed.

Patched Versions zip 9.0.5 and later introduce consistency options (vulnerable: 9.0.4 and earlier). However, header consistency checks remain disabled by default through 9.2.2. Upgrading alone does not protect installations.

Action Required Upgrade to 9.1.0 or later and explicitly configure enforce_header_consistency(true) when extracting untrusted archives. cargo audit does not detect this vulnerability as of November 2, 2026.


A critical vulnerability, CVE-2026-115905, has been disclosed in the widely used Rust zip crate (package, zip-rs/zip2).
GitHub Advisory GHSA-m665-v7pm-7grj assigned a CVSS v4.0 severity base score of 10.0 (Critical), affecting versions 9.0.4 and earlier, with the initial patch provided in version 9.0.5.
The NVD record was published on October 27, 2026 (JST) and remains “Awaiting Analysis” as of November 2, making 10.0 GitHub’s provisional score.
While patched versions introduced header consistency validation, this check is disabled by default, and important behavioral differences exist across releases.

ZIP Files Store Filenames in Two Locations

Within a ZIP archive, metadata for a single entry is recorded in two separate places.

LocationPositionContents
Local File Header (LFH)Immediately precedes each entry’s compressed dataFilename, compression method, size, etc.
Central Directory (CDFH)Summary index located at the end of the archiveFilename, size, offset to each LFH, etc.

Both headers are expected to contain identical filenames.
However, the ZIP specification does not mandate that readers verify consistency between the two structures, leading different tools to rely on different headers.
Tools capable of reading the entire archive can parse the Central Directory first, whereas streaming utilities that process files sequentially read Local File Headers before ever reaching the end-of-archive index.

Two APIs in the zip Crate Return Different Filenames

In the zip crate, different APIs relied on different headers.

APIHeader ConsultedPrimary Use Case
ZipArchiveCentral DirectoryWhole-archive listing, inspection, and extraction
ZipStreamReaderLocal File HeaderSequential streaming extraction

ZipArchive retrieves filenames exclusively from the Central Directory, while ZipStreamReader reads from the stream start and writes files using names in the Local File Headers without cross-checking the Central Directory.
Consequently, an archive crafted with doc_report.pdf in the Central Directory and cron_worker.sh in the Local File Header appears as two completely different files depending on the API used.
The advisory’s proof-of-concept (PoC) builds a ZIP containing cron_worker.sh with the zip crate itself, then overwrites the 14-byte filename in the trailing Central Directory with doc_report.pdf of the same length, leaving compressed payload data and CRC (checksum) intact.

flowchart TD
    A["Attacker crafts malicious ZIP<br/>Index: doc_report.pdf<br/>LFH: cron_worker.sh"] --> B["Inspector lists contents via ZipArchive"]
    B --> C["Only doc_report.pdf is visible<br/>PDF-only rule passes"]
    C --> D["Extractor unpacks via ZipStreamReader"]
    D --> E["cron_worker.sh in destination<br/>overwritten with attacker payload"]
    E --> F["RCE occurs if environment executes script"]

In the PoC, an inspection routine checks that all entries have a .pdf extension using ZipArchive. The crafted archive passes validation, but ZipStreamReader::extract subsequently unpacks cron_worker.sh, overwriting a pre-existing worker script with attacker-controlled content.

Scope of “Arbitrary File Overwrite”

The term “arbitrary file overwrite” here refers to replacing arbitrary files located within the designated destination directory.
ZipStreamReader::extract normalizes paths prior to writing and rejects entries that attempt to escape the destination directory (such as paths containing ../). The vulnerability does not circumvent directory traversal protections.
The PoC demonstrates overwriting cron_worker.sh existing directly inside the extraction target.

The vulnerability carries significant risk when extraction directories house executable scripts or configuration files.
The advisory highlights automated ZIP ingestion pipelines where scanning or whitelist validation occurs in a separate stage from extraction.

Another prerequisite is the use of ZipStreamReader.
This API resides in zip::unstable::stream, an explicitly unstable module. Applications relying exclusively on ZipArchive for both validation and extraction are unaffected by API discrepancy.
However, identical bypasses occur in multi-stage pipelines where external utilities (such as antivirus scanners reading Central Directories) perform inspection before ZipStreamReader unpacks the archive.

Header Consistency Disabled by Default in Patched Releases

The fix introduces the enforce_header_consistency configuration option (commit 8068ab4), which cross-checks filenames, compression methods, timestamps, CRC checksums, and entry sizes between headers.

However, this check is disabled by default.
Doc comments for ZipStreamReader::new in 9.0.5 state that Local File Headers are not verified against the Central Directory for backwards compatibility, initializing enforce_header_consistency: false.
This behavior remains unchanged in version 9.2.2 (latest as of November 2, 2026), and ZipReadOptions and ExtractOptions on ZipArchive also default to disabled.
Simply running cargo update to 9.0.5 or later does not alter the behavior of existing code.

To enable protection during extraction, applications must configure it explicitly:

use zip::unstable::stream::ZipStreamReader;

ZipStreamReader::new(reader)
    .enforce_header_consistency(true)
    .extract(&dest_dir)?;

During inspection, calling ZipArchive::validate_header_consistency() returns an error if header mismatches are detected (available starting in 9.0.5).
Executing this validation prior to iterating through entries rejects mismatched archives regardless of which downstream tool handles extraction.

let mut archive = zip::ZipArchive::new(reader)?;
archive.validate_header_consistency()?; // Returns Err on mismatch
for i in 0..archive.len() {
    // Proceed with entry inspection
}

Behavior in 9.0.5 vs Changes in 9.1.0

Because ZipStreamReader operates sequentially, the Central Directory needed for validation cannot be obtained until the entire archive has streamed through.
In 9.0.5, the reader writes each entry to disk while buffering header metadata, only validating against the Central Directory upon reaching EOF.
Consequently, even when consistency checks are enabled, cron_worker.sh has already been written to disk before an error is returned. Documentation for extract explicitly notes that extraction is not atomic and partial files may remain on error.
Although doc comments in 9.0.5 mention rollbacks, commit history reveals that partial extraction rollback was removed prior to release, and 9.0.5 does not clean up written files on mismatch.

Starting with 9.1.0 (released October 24, 2026), enabling consistency enforcement stages extracted files into a temporary directory inside the destination, promoting files to their final paths only after the full archive passes validation.
For extractor-level validation, upgrading to 9.1.0 or later before enabling consistency checks is essential.

VersionDefault Consistency SettingBehavior of ZipStreamReader::extract with Consistency Enabled
≤ 9.0.4Not presentNo cross-validation
9.0.5DisabledWrites files directly to destination, errors on mismatch (written files remain)
9.1.0–9.2.2DisabledExtracts into temporary directory, moves to final destination only on match

When using ZipStreamVisitor for custom event-driven extraction, visitors receive entries prior to archive-level verification. Handlers must avoid executing or evaluating file payloads until visit returns Ok.

Rust Requirements and Lack of cargo audit Detection

In 9.0.5, Cargo.toml sets the minimum supported Rust version (rust-version) to 1.88, continuing through 9.2.2. Projects pinned to older toolchains must update the Rust compiler before upgrading zip.

The cargo audit command scans dependencies against the RustSec Advisory Database.
As of November 2, 2026, the RustSec entry for the zip crate lists only three older advisories (RUSTSEC-2026-0367 through 0369); no record exists yet for GHSA-m665-v7pm-7grj.
Running cargo audit therefore will not flag this vulnerability at present. Dependabot and other GitHub Advisory Database consumers do alert on this issue.

The three registered RustSec advisories (SUID/SGID bit retention, symlink path traversal, and permissions modification via symlinks) were resolved in version 9.0.0; environments on versions prior to 9.0.0 remain exposed to those flaws as well.

Checking Your Projects

First, inspect which dependencies pull in the zip crate:

cargo tree -i zip

If Cargo.lock resolves multiple versions of zip, inspect each using cargo tree -i zip@<version>.
If zip is pulled in as a transitive dependency, the risk remains if upstream crates invoke ZipStreamReader without consistency checks.

Next, search your codebase for direct usages of ZipStreamReader or multi-stage extraction pipelines:

grep -rn "ZipStreamReader\|unstable::stream" --include='*.rs' .

If matching usages are found, update to 9.1.0 or later, pass enforce_header_consistency(true), and call validate_header_consistency() on the inspection side. Separating extraction directories from executable scripts and configuration files provides additional defense in depth.