critical

CVE

CVE-2026-115905, CVE-2026-115906, CVE-2026-115907, CVE-2026-115908, CVE-2026-115909, CVE-2026-105298, CVE-2026-105297

CWE

CWE-436, CWE-22, CWE-61, CWE-347, CWE-770, CWE-400

Affected Surface

  • Rust `zip` crate (`crates.io/crates/zip`, ~317M total downloads / ~92M recent downloads) versions `<= 9.0.4` for `CVE-2026-115905`, `0.6.0` before `9.0.0` for `CVE-2026-115906`, `5.1.0` before `9.0.0` for `CVE-2026-115907`, `7.2.0` before `9.0.0` for `CVE-2026-115908`, and `1.3.0` before `9.0.0` for `CVE-2026-115909`
  • Cloudflare `pingora-core` and `pingora` crates (`crates.io/crates/pingora-core`, ~10M downloads) versions `< 0.9.0` for `CVE-2026-105298` (`RUSTSEC-2026-0388` / `GHSA-mgg9-6xw2-jfgp`) and `< 0.8.1` for `CVE-2026-105297` (`RUSTSEC-2026-0389` / `GHSA-7gpj-mj4q-x62x`)
  • Rust services, artifact pipelines, security gateways, and update verifiers that validate ZIP archives with `ZipArchive` and extract with `ZipStreamReader` (or call `ZipArchive::extract` on untrusted archives), as well as Pingora reverse proxies and gateways terminating HTTP/2 (`h2` or `h2c`)

TL;DR

On 26 October 2026, NVD and RustSec published seven advisories across two foundational Rust crates: five CVEs in zip (zip-rs/zip2, with over 316 million total downloads and 92 million recent downloads on crates.io) and two CVEs in Cloudflare’s pingora-core HTTP proxy framework (~10 million downloads):

  1. zip (CVE-2026-115905, CVSS 10.0 Critical, plus CVE-2026-115906 through CVE-2026-115909):
    • CVE-2026-115905 (<= 9.0.4, fixed in 9.0.5): ZipArchive reads filenames and offsets from the Central Directory File Header (CDFH) at the end of the archive, whereas ZipStreamReader extracts sequentially using the Local File Header (LFH) at the start of each entry without reconciling the two headers. A crafted “Zip Cloaking” archive whose CDFH declares doc_report.pdf and whose LFH declares cron_worker.sh (or hides extra entries or overlaps the central directory) passes a ZipArchive allowlist check and then writes cron_worker.sh when extracted via ZipStreamReader. Importantly, the new enforce_header_consistency(true) check added in 9.0.5 is opt-in on ZipStreamReader::new() (enforce_header_consistency: false by default for backward compatibility), so streaming callers must explicitly enable it.
    • CVE-2026-115908 (7.2.0 to < 9.0.0, CVSS 7.4 High): ZipArchive::extract validated symlink targets with a lexical prefix comparison (simplified_components) rather than resolving intermediate symlinks on disk, allowing a two-step symlink chain (or a stripped Windows drive/UNC prefix) to write arbitrary files outside the extraction directory.
    • CVE-2026-115906 (0.6.0 to < 9.0.0, CVSS 6.5): AesReaderValid::read() in src/aes.rs only verified the 10-byte HMAC-SHA1-80 authentication tag when self.data_remaining reached 0 after reading payload bytes. Empty or truncated AES-encrypted entries returned Ok(0) without ever reading or comparing the HMAC tag.
    • CVE-2026-115909 and CVE-2026-115907 (< 9.0.0, CVSS 7.5 High): ZipArchive::extract called Vec::with_capacity(file.size() as usize) on an attacker-controlled ZIP64 uncompressed_size before reading a symlink target (and ShrinkDecoder, ReduceDecoder, and ImplodeDecoder called dst.reserve(uncompressed_size) when built with legacy-zip), causing an uncatchable allocator abort or capacity-overflow panic on a tiny input archive.
  2. Cloudflare pingora-core (CVE-2026-105298 / RUSTSEC-2026-0388 and CVE-2026-105297 / RUSTSEC-2026-0389):
    • CVE-2026-105298 (< 0.9.0): Pingora accepted HTTP/2 requests that carried both an :authority pseudo-header and a conflicting Host header (or userinfo@ in :authority). Because request_filter and cache keys inspected session.req_header().uri (:authority), while HTTP/1.1 upstreams received the raw Host header and HTTP/2 upstreams had their :authority overwritten by Host after filters ran, an unauthenticated attacker could bypass host-based access controls or poison the cache for another virtual host.
    • CVE-2026-105297 (< 0.8.1): When handshake(io, None) was called without custom H2Options, Pingora used the h2 crate’s unbounded defaults (16 MiB decoded header list and unlimited concurrent streams), letting a single HTTP/2 connection OOM-kill a 2 GiB proxy process via 1-byte HPACK header references.

What to do now:

  1. Upgrade zip to 9.0.5 or later (9.1.2 is current on crates.io), and if your code uses ZipStreamReader or ZipReadOptions, call .enforce_header_consistency(true) explicitly.
  2. Upgrade pingora-core and pingora to 0.9.0 or later, and if you construct custom H2Options, start from pingora_core::protocols::http::v2::server::default_h2_options() rather than H2Options::default().

Affected packages and projects

EcosystemCrateCVE / AdvisorySeverityAffected versionsFixed versionVulnerable component
crates.iozipCVE-2026-115905 (GHSA-m665-v7pm-7grj)CVSS 10.0 Critical<= 9.0.49.0.5 (with .enforce_header_consistency(true))src/read/stream.rs (ZipStreamReader), src/read/zip_archive.rs (has_overlapping_files)
crates.iozipCVE-2026-115908 (GHSA-hjqx-j4rw-g7xv)CVSS 7.4 High>= 7.2.0, < 9.0.09.0.0src/path.rs (simplified_components), src/read/zipfile.rs (safe_prepare_path)
crates.iozipCVE-2026-115909 (GHSA-q7v9-x93c-xqv3)CVSS 7.5 High>= 1.3.0, < 9.0.09.0.0src/read/mod.rs (ZipArchive::extract symlink Vec::with_capacity)
crates.iozipCVE-2026-115907 (GHSA-9hj8-73fh-9963)CVSS 7.5 High>= 5.1.0, < 9.0.0 (legacy-zip feature)9.0.0src/legacy/{shrink,reduce,implode}.rs (dst.reserve(uncompressed_size))
crates.iozipCVE-2026-115906 (GHSA-8hxf-qr8p-62x4)CVSS 6.5 Medium>= 0.6.0, < 9.0.09.0.0src/aes.rs (AesReaderValid::read missing HMAC check on EOF / 0-byte entry)
crates.iopingora-core, pingoraCVE-2026-105298 (RUSTSEC-2026-0388, GHSA-mgg9-6xw2-jfgp)High< 0.9.00.9.0pingora-core/src/protocols/http/v2/server.rs and authority.rs
crates.iopingora-core, pingoraCVE-2026-105297 (RUSTSEC-2026-0389, GHSA-7gpj-mj4q-x62x)High< 0.8.10.8.1pingora-core/src/protocols/http/v2/server.rs (handshake H2Options defaults)

Technical analysis

1. CVE-2026-115905: dual-header Zip Cloaking between ZipArchive and ZipStreamReader

The ZIP format stores file metadata in two places: a Local File Header (LFH) immediately preceding each entry’s compressed data stream, and a Central Directory File Header (CDFH) at the end of the archive. In the Rust zip crate:

  • ZipArchive::new(reader) seeks to the end of the archive, parses the Central Directory, and exposes entry names, sizes, and extra fields from the CDFH.
  • ZipStreamReader::new(reader) reads the archive sequentially from byte 0 without seeking, extracting entries as it encounters each LFH and only reading the Central Directory at the end of the stream to pass metadata to visit_additional_metadata().

Before commit 8068ab480c (v9.0.5), ZipStreamReader never verified that the LFH entries it extracted matched the CDFH entries at the end of the archive. The regression test added in tests/header_consistency.rs demonstrates how an archive can pass a security gateway that inspects ZipArchive while writing an executable script when extracted by ZipStreamReader:

// tests/header_consistency.rs in zip-rs/zip2@8068ab480c (excerpt)
/// Constructs a dual-header archive where LFH is "cron_worker.sh" and CDFH is "doc_report.pdf".
fn create_dual_header_archive() -> Vec<u8> {
    const SCRIPT_NAME: &str = "cron_worker.sh";
    const SPOOFED_NAME: &str = "doc_report.pdf";
    ...
}

The same commit closes four related header-desynchronization vectors:

  1. LFH vs. CDFH reconciliation and rollback: When .enforce_header_consistency(true) is set on ZipStreamReader or ZipReadOptions, visit() records HeaderConsistencyMetadata (raw filename, 0x7075 Info-ZIP UnicodePath extra field and CRC32, compression method, encryption flag, data-descriptor flag, timestamp, CRC32, compressed/uncompressed sizes, and 0x9901 AES parameters) plus the physical stream offset for every LFH, then compares them 1-to-1 against the sorted CDFH entries at the end of the stream. If ZipStreamReader::extract() detects a mismatch, it rolls back and deletes every file and directory created during the streaming pass.
  2. Central Directory overlap check in has_overlapping_files(): Previously, ZipArchive::has_overlapping_files() only sorted entry byte ranges and checked ranges.windows(2).any(|w| w[0].end > w[1].start). It did not check whether an entry’s declared payload extended past self.shared.dir_start into the Central Directory itself:
    --- a/src/read/zip_archive.rs
    +++ b/src/read/zip_archive.rs
    @@ -515,7 +520,8 @@ impl<R: Read + Seek> ZipArchive<R> {
             ranges.push(start..end);
         }
         ranges.sort_unstable_by_key(|r| (r.start, r.end));
    -    Ok(ranges.windows(2).any(|w| w[0].end > w[1].start))
    +    let overlaps_central_directory = ranges.iter().any(|r| r.end > self.shared.dir_start);
    +    Ok(overlaps_central_directory || ranges.windows(2).any(|w| w[0].end > w[1].start))
     }
  3. Duplicate 0x7075 UnicodePath CRC check and 0x9901 AES extra field validation: In src/extra_fields/extra_field.rs, apply_extra_fields() previously hashed file_name_raw on each UnicodePath extra field after an earlier UnicodePath field had already mutated file_name_raw, and accepted ExtraField::AeXEncryption even when the entry’s outer compression method was not CompressionMethod::Aes (99).

Operational caveat for Rust developers: In zip 9.0.5 (and 9.1.2), ZipStreamReader::new(reader) still initializes enforce_header_consistency: false by default:

// src/read/stream.rs in zip 9.0.5
impl<R> ZipStreamReader<R> {
    /// Create a new `ZipStreamReader` with default settings. For backward-compatibility,
    /// this doesn't check that the local headers match the central directory.
    pub const fn new(reader: R) -> Self {
        Self {
            reader,
            enforce_header_consistency: false,
        }
    }
}

Simply bumping Cargo.lock to zip 9.0.5 is not enough if your code calls ZipStreamReader::new(r).extract(dst) or ZipStreamReader::new(r).visit(&mut visitor). You must chain .enforce_header_consistency(true).

2. CVE-2026-115908, CVE-2026-115906, CVE-2026-115909, and CVE-2026-115907 in zip 9.0.0

The four companion CVEs fixed in zip 9.0.0 target archive extraction and decryption:

  • Symlink chain escape (CVE-2026-115908, commit 09c030eab5): From 7.2.0 to 8.x, safe_prepare_path validated symlink targets lexically using simplified_components(), which stripped Windows drive/UNC prefixes (C:\, \\?\) and did not resolve intermediate symlinks already on disk. An attacker who knew or guessed the absolute extraction path could create a symlink whose lexical target appeared to stay under the destination directory while traversing through an intermediate symlink to overwrite arbitrary files. 9.0.0 replaces lexical prefix checking with resolve_enclosed() and strip_base_prefix().
  • Unverified AES HMAC on truncated or empty entries (CVE-2026-115906, commit ec265db0b0): In src/aes.rs, AesReaderValid::read() decremented self.data_remaining -= read as u64 and only checked the 10-byte WinZip AE-1/AE-2 HMAC-SHA1-80 tag when self.data_remaining == 0 after a non-zero read. If an encrypted entry had a 0-byte payload (self.data_length == 0) or the underlying reader hit EOF early while self.data_remaining > 0, read() returned Ok(0) without ever calling verify_hmac().
  • Uncatchable OOM abort on symlink and legacy-zip pre-allocation (CVE-2026-115909 and CVE-2026-115907, commits a543a1fc8f and 8b4991288b): In ZipArchive::extract(), any entry marked as a symlink ran let mut target = Vec::with_capacity(file.size() as usize); using the untrusted 64-bit uncompressed_size field from the central directory before reading a single byte. Because Vec::with_capacity aborts the entire Rust process on allocation failure (which std::panic::catch_unwind cannot catch), a ~100-byte ZIP file claiming a multi-gigabyte symlink target crashed the server. 9.0.0 caps symlink targets at MAX_SYMLINK_TARGET_LEN = 4096 and caps legacy-zip (Shrink, Reduce, Implode) speculative pre-allocation at MAX_PREALLOC = 1 << 20 (1 MiB).

3. CVE-2026-105298 and CVE-2026-105297: HTTP/2 :authority vs. Host desync and HPACK OOM in Cloudflare pingora-core

In HTTP/2 (RFC 9113 Section 8.3.1), the :authority pseudo-header replaces the HTTP/1.1 Host header, and an intermediary must not forward a conflicting Host header.

Before pingora-core 0.9.0 (commit 915590a918 and commit ffab8302d0), Pingora’s HTTP/2 server accepted requests containing both :authority: allowed.example.com and Host: internal.example.com (as well as :authority values containing userinfo@ such as user@allowed.example.com):

  • During request filtering, authentication, logging, and cache-key generation, Pingora middleware reading session.req_header().uri saw allowed.example.com.
  • When forwarding the request over HTTP/1.1 to an upstream server, Pingora only synthesized a Host header from :authority if no Host header was already present in the header map, so the conflicting client Host: internal.example.com was sent straight to the upstream.
  • When forwarding over HTTP/2 to an upstream server, Pingora removed the client Host header after request_filter had already executed and used that Host value to overwrite the upstream :authority pseudo-header.

The fix in pingora-core/src/protocols/http/authority.rs and v2/server.rs rejects any request with duplicate Host headers, userinfo (@) in the authority, or a Host header that does not match :authority with HTTP 400 Bad Request (and tears down connections that exceed MAX_MALFORMED_STREAMS_PER_CONN = 32).

Separately, CVE-2026-105297 (RUSTSEC-2026-0389, fixed in 0.8.1 via commit d193c8d49b) occurred because pingora_core::protocols::http::v2::server::handshake() fell back to options.unwrap_or_default(), which used the upstream h2 crate’s defaults: a 16 MiB max_header_list_size and no limit on max_concurrent_streams:

--- a/pingora-core/src/protocols/http/v2/server.rs
+++ b/pingora-core/src/protocols/http/v2/server.rs
@@ -42,12 +42,28 @@ type H2Connection<S> = server::Connection<S, Bytes>;
+// 64 KiB decoded header-list limit.
+const DEFAULT_MAX_HEADER_LIST_SIZE: u32 = 64 * 1024;
+const DEFAULT_MAX_CONCURRENT_STREAMS: u32 = 100;
+
+pub fn default_h2_options() -> H2Options {
+    let mut options = H2Options::default();
+    options.max_header_list_size(DEFAULT_MAX_HEADER_LIST_SIZE);
+    options.max_concurrent_streams(DEFAULT_MAX_CONCURRENT_STREAMS);
+    options
+}
...
 pub async fn handshake(io: Stream, options: Option<H2Options>) -> Result<H2Connection<Stream>> {
-    let options = options.unwrap_or_default();
+    let options = options.unwrap_or_else(default_h2_options);
     let res = options.handshake(io).await;

Detection and remediation

# 1. Check Cargo.lock files for vulnerable zip (<= 9.0.4) and pingora-core (< 0.9.0) versions
rg -n -A 2 'name = "(zip|pingora|pingora-core|pingora-proxy)"' Cargo.lock 2>/dev/null

# 2. Search Rust source code for ZipStreamReader or ZipReadOptions usage that lacks enforce_header_consistency(true)
rg -n 'ZipStreamReader|read_zipfile_from_stream|by_name_with_options|by_index_with_options' src crates 2>/dev/null

# 3. Search Pingora services for custom H2Options::default() calls that bypass default_h2_options()
rg -n 'H2Options::default|max_header_list_size|max_concurrent_streams' src crates 2>/dev/null
  1. For zip consumers: Upgrade to zip >= 9.0.5 (cargo update -p zip), and audit every call to ZipStreamReader::new(reader) or ZipReadOptions::default() to chain .enforce_header_consistency(true) whenever an archive validated by Central Directory inspection is later processed by a streaming reader.
  2. For pingora / pingora-core consumers: Upgrade to pingora-core >= 0.9.0 (cargo update -p pingora-core). If you cannot upgrade immediately, reject HTTP/2 requests in request_filter when session.req_header().headers.get(http::header::HOST) differs from session.req_header().uri.authority() or when the authority contains '@', and ensure any custom H2Options starts from default_h2_options() or sets max_header_list_size(65536) and max_concurrent_streams(100).

Timeline

  • 2026-06-04 to 2026-08-11: Cloudflare merges d193c8d49b (CVE-2026-105297 in pingora 0.8.1) and 915590a918 / ffab8302d0 (CVE-2026-105298 in pingora 0.9.0).
  • 2026-09-14 to 2026-10-09: zip-rs/zip2 merges fixes for CVE-2026-115909 (a543a1fc8f), CVE-2026-115907 (8b4991288b), CVE-2026-115908 (09c030eab5), and CVE-2026-115906 (ec265db0b0), shipping them in zip v9.0.0 on 9 October 2026.
  • 2026-10-16 to 2026-10-17: zip-rs/zip2 merges commit 8068ab480c (enforce_header_consistency and Central Directory overlap checks for CVE-2026-115905) and publishes zip v9.0.5.
  • 2026-10-26: CVE-2026-115905 (CVSS 10.0), CVE-2026-115906 through CVE-2026-115909, and RUSTSEC-2026-0388 / RUSTSEC-2026-0389 (CVE-2026-105298 and CVE-2026-105297) are published to NVD and the RustSec Advisory Database.

From research to remediation

Check whether this pattern exists in your codebase

Turn this research into a remediation workflow. Scan dependencies and package manifests for similar supply-chain risk, then prioritize fixes with reachability context.

References