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):
zip(CVE-2026-115905, CVSS 10.0 Critical, plusCVE-2026-115906throughCVE-2026-115909):CVE-2026-115905(<= 9.0.4, fixed in9.0.5):ZipArchivereads filenames and offsets from the Central Directory File Header (CDFH) at the end of the archive, whereasZipStreamReaderextracts 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 declaresdoc_report.pdfand whose LFH declarescron_worker.sh(or hides extra entries or overlaps the central directory) passes aZipArchiveallowlist check and then writescron_worker.shwhen extracted viaZipStreamReader. Importantly, the newenforce_header_consistency(true)check added in9.0.5is opt-in onZipStreamReader::new()(enforce_header_consistency: falseby default for backward compatibility), so streaming callers must explicitly enable it.CVE-2026-115908(7.2.0to< 9.0.0, CVSS 7.4 High):ZipArchive::extractvalidated 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.0to< 9.0.0, CVSS 6.5):AesReaderValid::read()insrc/aes.rsonly verified the 10-byte HMAC-SHA1-80 authentication tag whenself.data_remainingreached0after reading payload bytes. Empty or truncated AES-encrypted entries returnedOk(0)without ever reading or comparing the HMAC tag.CVE-2026-115909andCVE-2026-115907(< 9.0.0, CVSS 7.5 High):ZipArchive::extractcalledVec::with_capacity(file.size() as usize)on an attacker-controlled ZIP64uncompressed_sizebefore reading a symlink target (andShrinkDecoder,ReduceDecoder, andImplodeDecodercalleddst.reserve(uncompressed_size)when built withlegacy-zip), causing an uncatchable allocator abort or capacity-overflow panic on a tiny input archive.
- Cloudflare
pingora-core(CVE-2026-105298/RUSTSEC-2026-0388andCVE-2026-105297/RUSTSEC-2026-0389):CVE-2026-105298(< 0.9.0): Pingora accepted HTTP/2 requests that carried both an:authoritypseudo-header and a conflictingHostheader (oruserinfo@in:authority). Becauserequest_filterand cache keys inspectedsession.req_header().uri(:authority), while HTTP/1.1 upstreams received the rawHostheader and HTTP/2 upstreams had their:authorityoverwritten byHostafter 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): Whenhandshake(io, None)was called without customH2Options, Pingora used theh2crate’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:
- Upgrade
zipto9.0.5or later (9.1.2is current on crates.io), and if your code usesZipStreamReaderorZipReadOptions, call.enforce_header_consistency(true)explicitly. - Upgrade
pingora-coreandpingorato0.9.0or later, and if you construct customH2Options, start frompingora_core::protocols::http::v2::server::default_h2_options()rather thanH2Options::default().
Affected packages and projects
| Ecosystem | Crate | CVE / Advisory | Severity | Affected versions | Fixed version | Vulnerable component |
|---|---|---|---|---|---|---|
| crates.io | zip | CVE-2026-115905 (GHSA-m665-v7pm-7grj) | CVSS 10.0 Critical | <= 9.0.4 | 9.0.5 (with .enforce_header_consistency(true)) | src/read/stream.rs (ZipStreamReader), src/read/zip_archive.rs (has_overlapping_files) |
| crates.io | zip | CVE-2026-115908 (GHSA-hjqx-j4rw-g7xv) | CVSS 7.4 High | >= 7.2.0, < 9.0.0 | 9.0.0 | src/path.rs (simplified_components), src/read/zipfile.rs (safe_prepare_path) |
| crates.io | zip | CVE-2026-115909 (GHSA-q7v9-x93c-xqv3) | CVSS 7.5 High | >= 1.3.0, < 9.0.0 | 9.0.0 | src/read/mod.rs (ZipArchive::extract symlink Vec::with_capacity) |
| crates.io | zip | CVE-2026-115907 (GHSA-9hj8-73fh-9963) | CVSS 7.5 High | >= 5.1.0, < 9.0.0 (legacy-zip feature) | 9.0.0 | src/legacy/{shrink,reduce,implode}.rs (dst.reserve(uncompressed_size)) |
| crates.io | zip | CVE-2026-115906 (GHSA-8hxf-qr8p-62x4) | CVSS 6.5 Medium | >= 0.6.0, < 9.0.0 | 9.0.0 | src/aes.rs (AesReaderValid::read missing HMAC check on EOF / 0-byte entry) |
| crates.io | pingora-core, pingora | CVE-2026-105298 (RUSTSEC-2026-0388, GHSA-mgg9-6xw2-jfgp) | High | < 0.9.0 | 0.9.0 | pingora-core/src/protocols/http/v2/server.rs and authority.rs |
| crates.io | pingora-core, pingora | CVE-2026-105297 (RUSTSEC-2026-0389, GHSA-7gpj-mj4q-x62x) | High | < 0.8.1 | 0.8.1 | pingora-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 byte0without seeking, extracting entries as it encounters each LFH and only reading the Central Directory at the end of the stream to pass metadata tovisit_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:
- LFH vs. CDFH reconciliation and rollback: When
.enforce_header_consistency(true)is set onZipStreamReaderorZipReadOptions,visit()recordsHeaderConsistencyMetadata(raw filename,0x7075Info-ZIPUnicodePathextra field and CRC32, compression method, encryption flag, data-descriptor flag, timestamp, CRC32, compressed/uncompressed sizes, and0x9901AES 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. IfZipStreamReader::extract()detects a mismatch, it rolls back and deletes every file and directory created during the streaming pass. - Central Directory overlap check in
has_overlapping_files(): Previously,ZipArchive::has_overlapping_files()only sorted entry byte ranges and checkedranges.windows(2).any(|w| w[0].end > w[1].start). It did not check whether an entry’s declared payload extended pastself.shared.dir_startinto 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)) } - Duplicate
0x7075UnicodePathCRC check and0x9901AES extra field validation: Insrc/extra_fields/extra_field.rs,apply_extra_fields()previously hashedfile_name_rawon eachUnicodePathextra field after an earlierUnicodePathfield had already mutatedfile_name_raw, and acceptedExtraField::AeXEncryptioneven when the entry’s outer compression method was notCompressionMethod::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, commit09c030eab5): From7.2.0to8.x,safe_prepare_pathvalidated symlink targets lexically usingsimplified_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.0replaces lexical prefix checking withresolve_enclosed()andstrip_base_prefix(). - Unverified AES HMAC on truncated or empty entries (
CVE-2026-115906, commitec265db0b0): Insrc/aes.rs,AesReaderValid::read()decrementedself.data_remaining -= read as u64and only checked the 10-byte WinZip AE-1/AE-2 HMAC-SHA1-80 tag whenself.data_remaining == 0after a non-zero read. If an encrypted entry had a 0-byte payload (self.data_length == 0) or the underlying reader hit EOF early whileself.data_remaining > 0,read()returnedOk(0)without ever callingverify_hmac(). - Uncatchable OOM abort on symlink and legacy-zip pre-allocation (
CVE-2026-115909andCVE-2026-115907, commitsa543a1fc8fand8b4991288b): InZipArchive::extract(), any entry marked as a symlink ranlet mut target = Vec::with_capacity(file.size() as usize);using the untrusted 64-bituncompressed_sizefield from the central directory before reading a single byte. BecauseVec::with_capacityaborts the entire Rust process on allocation failure (whichstd::panic::catch_unwindcannot catch), a ~100-byte ZIP file claiming a multi-gigabyte symlink target crashed the server.9.0.0caps symlink targets atMAX_SYMLINK_TARGET_LEN = 4096and capslegacy-zip(Shrink,Reduce,Implode) speculative pre-allocation atMAX_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().urisawallowed.example.com. - When forwarding the request over HTTP/1.1 to an upstream server, Pingora only synthesized a
Hostheader from:authorityif noHostheader was already present in the header map, so the conflicting clientHost: internal.example.comwas sent straight to the upstream. - When forwarding over HTTP/2 to an upstream server, Pingora removed the client
Hostheader afterrequest_filterhad already executed and used thatHostvalue to overwrite the upstream:authoritypseudo-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
- For
zipconsumers: Upgrade tozip >= 9.0.5(cargo update -p zip), and audit every call toZipStreamReader::new(reader)orZipReadOptions::default()to chain.enforce_header_consistency(true)whenever an archive validated by Central Directory inspection is later processed by a streaming reader. - For
pingora/pingora-coreconsumers: Upgrade topingora-core >= 0.9.0(cargo update -p pingora-core). If you cannot upgrade immediately, reject HTTP/2 requests inrequest_filterwhensession.req_header().headers.get(http::header::HOST)differs fromsession.req_header().uri.authority()or when the authority contains'@', and ensure any customH2Optionsstarts fromdefault_h2_options()or setsmax_header_list_size(65536)andmax_concurrent_streams(100).
Timeline
- 2026-06-04 to 2026-08-11: Cloudflare merges
d193c8d49b(CVE-2026-105297inpingora0.8.1) and915590a918/ffab8302d0(CVE-2026-105298inpingora0.9.0). - 2026-09-14 to 2026-10-09:
zip-rs/zip2merges fixes forCVE-2026-115909(a543a1fc8f),CVE-2026-115907(8b4991288b),CVE-2026-115908(09c030eab5), andCVE-2026-115906(ec265db0b0), shipping them inzipv9.0.0on 9 October 2026. - 2026-10-16 to 2026-10-17:
zip-rs/zip2merges commit8068ab480c(enforce_header_consistencyand Central Directory overlap checks forCVE-2026-115905) and publisheszipv9.0.5. - 2026-10-26:
CVE-2026-115905(CVSS 10.0),CVE-2026-115906throughCVE-2026-115909, andRUSTSEC-2026-0388/RUSTSEC-2026-0389(CVE-2026-105298andCVE-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
- zip-rs/zip2 commit 8068ab480c: enforce header consistency between LFH and CDFH (CVE-2026-115905)
- zip-rs/zip2 commit 09c030eab5: resolve intermediate symlinks during extraction (CVE-2026-115908)
- zip-rs/zip2 commit ec265db0b0: verify AES HMAC on empty or truncated entries (CVE-2026-115906)
- zip-rs/zip2 commit a543a1fc8f: bound symlink target pre-allocation to MAX_SYMLINK_TARGET_LEN (CVE-2026-115909)
- zip-rs/zip2 commit 8b4991288b: cap speculative pre-allocation in legacy decoders (CVE-2026-115907)
- cloudflare/pingora commits 915590a918 and ffab8302d0: reject conflicting HTTP/2 :authority and Host headers (CVE-2026-105298)
- cloudflare/pingora commit d193c8d49b: bound default HTTP/2 header-list size and concurrent streams (CVE-2026-105297)
- RustSec advisories RUSTSEC-2026-0388 and RUSTSEC-2026-0389 (pingora-core)
- NVD: CVE-2026-115905 and CVE-2026-105298