Repository navigation
chore(deps): update rust crate zip to v9 [security] - #1111
Open
renovate[bot] wants to merge 1 commit into
Open
renovate[bot] wants to merge 1 commit into
renovate[bot] wants to merge 1 commit into
Conversation
✅ Deploy Preview for angry-raman-7c44f6 canceled.
|
renovate
Bot
force-pushed
the
renovate/crate-zip-vulnerability
branch
13 times, most recently
from
November 1, 2026 14:02
07573de to
f50bd0d
Compare
renovate
Bot
force-pushed
the
renovate/crate-zip-vulnerability
branch
2 times, most recently
from
November 1, 2026 15:08
c4fe3c8 to
a0399ae
Compare
renovate
Bot
force-pushed
the
renovate/crate-zip-vulnerability
branch
from
November 1, 2026 22:12
a0399ae to
7b86c7a
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
This PR body was truncated due to platform limits.
This PR contains the following updates:
8.6→9.0zip: Critical Parser Differential & Security Control Bypass via Dual-Header Inconsistencies (Zip Cloaking) Leading to Arbitrary File Overwrite & RCE
CVE-2026-115905 / GHSA-m665-v7pm-7grj
More information
Details
Summary
A critical architectural parser differential exists in the
zipcrate (<= 9.0.4) between random-access archive readers (ZipArchive) and streaming readers (ZipStreamReader).ZipArchiveconsiders the Central Directory File Header (CDFH) authoritative and ignores the filename declared in the Local File Header (LFH). In contrast,ZipStreamReaderrelies exclusively on the Local File Header (LFH) when extracting files to the filesystem.This discrepancy enables ZIP archives to appear to have different file paths and contents according to which tool is reading them.
Details
Architectural Root Cause
In the PKWARE ZIP archive format, every file entry is defined in two distinct locations:
In
zip2:src/read/zip_archive.rs:ZipArchiveparses the central directory intoSharedBuilder.files. All file lookups by index or name read exclusively from the CDFH table, completely ignoring the filename present in the LFH.src/read/stream.rs:L86-L141:ZipStreamReader::visitandZipStreamReader::extractread sequentially from the stream without verifying the central directory index. Each entry is parsed viaread_zipfile_from_streamand written to disk using the unverified filename declared in the LFH:src/write.rs:L1552: The crate explicitly exposesshallow_copy_file, an API that constructs dual-header desynchronizations by adding a new CDFH entry with an alias name pointing to an existing file without updating the original LFH.Threat Scenario & Severity Justification (10.0 / 9.9 Critical)
doc_report.pdf(Clean document)cron_worker.sh(Malicious root backdoor)ZipArchive(or similar CDFH-first parsers), detects onlydoc_report.pdf, and approves the file.ZipStreamReader::extract.ZipStreamReaderreads the LFH and overwritescron_worker.sh.Proof of Concept
The following automated integration test reproducibly demonstrates the complete CVSS 10.0 / 9.9 exploit chain (Security Gateway Evasion -> Arbitrary File Overwrite -> RCE & DoS) against
zip2:Verified Test Execution Output
Impact
An attacker can construct a crafted archive whose Central Directory displays a benign filename (doc_report.pdf) that is inspected and approved by upstream security gateways (antivirus scanners, mail filters, upload firewalls), while automated backend ingestion pipelines or microservices using ZipStreamReader extract an arbitrary or malicious file (overwriting e.g. cron_worker.sh or critical system binaries). Because an adversary can overwrite existing files on disk, this vulnerability leads to Security Control Bypass, Arbitrary File Overwrite, Complete Denial of Service, and Remote Code Execution (RCE) in situations where a tool relying on the central directory trusts a tool that relied on the local file headers or vice-versa.
Who is Impacted:
Severity
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:LReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
zip: Unbounded Symlink-Target Handling Can Abort Or Exhaust Memory During Extraction
CVE-2026-115909 / GHSA-q7v9-x93c-xqv3
More information
Details
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 withSIGABRTthrough this infallible allocation.Details
src/read/mod.rs#L399-L405(reviewed commit6f09b306); the same code is atsrc/read.rs#L417-L418in released v8.6.0.Both inputs are attacker-controlled:
ZipFile::size()returnsuncompressed_sizeverbatim from the central directory record. A ZIP64 extended-information extra field widens it to a fullu64(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 theS_IFLNKbit.Memory allocation failure → uncatchable process abort. The reservation is sized from the declared
uncompressed_sizebefore a single byte of the entry is read.Vec::with_capacity()is infallible: on allocator refusal it callshandle_alloc_error(), which aborts the process. This is not a Rust panic — it is not caught bycatch_unwind(), is unaffected bypanic = "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^38upward abort reliably. Sizes at or above2^63instead tripVec's internal capacity check and produce a catchablecapacity overflowpanic, so an attacker simply avoids that band. The reproducer declares2^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 aulimit -vcap or a memory-constrained container the threshold drops far enough that a plain 32-bituncompressed_sizewith 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
zipharness and payload. It requires only thezipcrate; use a standard Base64 decoder to materialise the archive before running it.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.zipon Linux), and runcargo run --release -- archive.zip.Uncatchable abort — 112 bytes. One
Storedentry nameda; external attributes0xA1FF0000(S_IFLNK|0777); ZIP64 extra field declaringuncompressed_size = 2^62; zero-byte payload. sha2560c68c3934f22651a6bd2726f7d58140124b1839196a1d0ae468864cdfda74676.Observed on x86-64 Linux, rustc 1.93.1, release profile, against both the reviewed commit and released v8.6.0:
The
SURVIVEDline never prints. Wrappingextract()incatch_unwindinside a spawned thread withpanic = "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 withcatch_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, replaceVec::with_capacity()with fallibletry_reserve()and return an I/O error when allocation fails. Do not use Linux'sPATH_MAXas a cross-platform contract.Apply the allocation fix to
src/read/stream.rsas 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 suspiciousnumber_of_filesto 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 ofVec::with_capacityacrosssrc/finds exactly two sites sized from untrusted input:src/read/mod.rs:401andsrc/read/stream.rs:73.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
zip: AES Entry HMAC Authentication Bypass on Premature EOF
CVE-2026-115906 / GHSA-8hxf-qr8p-62x4
More information
Details
Summary
An AES-encrypted (AE-x) entry's authentication code is verified only when the reader's byte counter lands exactly on zero. If the bounded entry reader reaches the archive's actual EOF before the attacker-controlled, header-declared
compressed_size, the counter remains positive, the HMAC is never read or compared, and the truncated, unauthenticated plaintext is returned as a clean end of stream. A zero-payload entry also returns before authentication. An attacker who does not know the password can modify an archive so chosen or modified plaintext is accepted with no error.Details
src/aes.rs:246-292:data_remainingstarts atcompressed_size - (2 + 10 + salt_length)(src/aes.rs:144-145), wherecompressed_sizecomes straight from the attacker-controlled central directory. Nothing cross-checks the declared data span against the archive's structural boundaries:ZipFileData::find_content()is justreader.take(self.compressed_size)(src/types.rs:214-223), andmake_crypto_reader()passes the declared value intoAesReader::new()(src/read/readers.rs).When the entry's bounded reader reaches true EOF before the declared size, the inner reader returns
Ok(0), the subtraction takes away nothing, and the finalisation block — the only place the 10-byte HMAC-SHA1-80 tag is read and compared — never runs. If bytes such as the central directory follow the entry, they are consumed as payload until theTakelimit is reached; the HMAC then usually fails. The bypass therefore requires true EOF at the premature end (as in the PoC) or the zero-payload case. The early return at the top ofread()means an entry whose declared size leaves zero payload bytes is never authenticated either, even though its tag is present in the file.This defeats the guarantee the type documents: "After the data has been read and decrypted an HMAC will be checked and provide a final means to check if either the password is invalid or if the data has been changed."
AE-2 entries skip the CRC-32 check by design (
src/read/readers.rs→Crc32Reader::new(.., should_disable = true)), so an AE-2 entry on this path has no integrity check of any kind remaining. AES-CTR is malleable, so an attacker who can modify an archive in transit — without the password — can XOR chosen plaintext onto the ciphertext and arrange for the entry to end at true EOF. The password-bearing read paths are affected, includingby_index_decrypt(),by_name_decrypt(),by_path_decrypt(), andby_index_with_options()when a password is supplied. Bareextract()callsby_index()without a password and therefore rejects AES-encrypted entries; an application that decrypts entries through these APIs before writing or extracting them is affected.PoC
tests/aes_hmac_bypass.rsbelow;Cargo.tomlneeds onlyzip = { git = "https://github.com/zip-rs/zip2", rev = "6f09b3064ec87c3a69ea9f3413f9f9638b4aa446" }, orzip = "8.6.0"to run it against the published 8.6.0 release unchanged. All three tests pass in debug and release against the reviewed commit.Turning the genuine plaintext into a chosen plaintext needs the genuine plaintext, as always with CTR malleability; without it the attacker still controls arbitrary bit-flips in the delivered content.
Impact
Any application that decrypts AES ZIP entries from untrusted or in-transit archives — backup restore, artefact and package ingestion, mail attachments, upload handling — can be served modified or attacker-chosen content by a party who does not hold the password, with no error surfaced by the affected read path. Exact chosen plaintext requires knowledge of the original plaintext, as with standard CTR malleability; otherwise the attacker can still induce arbitrary bit flips. Confidentiality is not affected and there is no memory-safety issue; the loss is the integrity guarantee that AE-x authenticated encryption exists to provide.
Suggested remediation
AesReaderValid::read(), treat a short inner read as an error rather than a clean EOF: after the decrement, ifread == 0 && self.data_remaining > 0, returnErrorKind::UnexpectedEof.data_length == 0.ZipFileData::find_content()/find_content_seek()(src/types.rs). Also make thecompressed_size - overheadsubtraction inAesReader::new()(src/aes.rs:144) checked — today a declared size below the AES overhead underflows it, which panics underoverflow-checksand, for sizes that still permit header validation, otherwise yields up to 9 bytes of unauthenticated plaintext through the same missing check.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
zip: Arbitrary file write outside the extraction directory, because ZipArchive::extract validates symlink targets lexically.
CVE-2026-115908 / GHSA-hjqx-j4rw-g7xv
More information
Details
Summary
9.0.0-pre37.2.0and laterThe validation applied to symlinks is purely lexical.
As a result, an attacker can write arbitrary files outside the extraction directory through a symlink.
Exploitation requires the attacker to know (or guess) the absolute path of the destination, but this is realistic in practice: CI workspaces,
/opt/app, a fixed Downloads path, and so on.Relationship to CVE-2025-29787
This is not a duplicate of the existing CVE-2025-29787 (2025-03); rather, the protection introduced for that CVE was subsequently lost.
Timeline (git history, UTC)
d4539d6b0199ac2csimplified_componentsv2.3.0d5cf4b55simplified_componentshandles absolute paths, from rejecting them to silently stripping them (released inv7.2.0)Why v2.3.0 was not affected, and how the protection was lost
The
safe_prepare_pathin v2.3.0 performs essentially the same lexical check as the current code. However,simplified_componentsat that time rejected any path containing an absolute component, so an absolute symlink target was never accepted in the first place and this attack path did not work.d5cf4b55was intended to make archives containing absolute path names extractable, but as a side effect absolute symlink targets now pass the lexical check, which silently removed the protection against CVE-2025-29787.Verification
The following three cases were compared using the same build (
9.0.0-pre3):parent -> ../)Err(InvalidArchive("Invalid symlink target path"))(the CVE fix itself is effective)Ok(()), writing outside the destinationd5cf4b55change reverted toreturn NoneErr(InvalidArchive("Invalid symlink target path"))The first row shows that the fix for the CVE itself still works, confirming that this report is not a duplicate.
The third row confirms that this vulnerability originates from the change in
d5cf4b55.Differences from CVE-2025-29787
AC:H). The CVE worked with a relative../and had no such precondition.ZipArchive::extract(andextract_unwrapped_root_dir).ZipStreamReader::extractis not affected, becauseexternal_attributesin the local header is0and thereforeis_symlink()never returns true (the CVE affected both APIs).Details
Lexical validation of the path string
The symlink validation in
src/read/zipfile.rs: L290-345is lexical.src/read/zipfile.rs: fn safe_prepare_path(L316-321):simplified_componentsdiscards the information that a path is absolute (drive prefix / root), normalizes it lexically, and then merely performs a prefix comparison between the two results. It never looks at how the path actually resolves on the file system.Consequently, the classic Zip Slip vector using
..is blocked, but paths that involve symlink resolution are not.src/path.rs: fn simplified_components(L9~26):Insufficient validation of
targetOnce the path check passes,
src/read/zipfile.rs: fn safe_prepare_path(L345) performsoutpath.push(target), but whether any intermediate component oftargetis itself a symlink is never checked.In addition,
make_symlink, which is called fromextract_internal, delegates tomake_symlink_impland only validates that the archive-suppliedtargetis valid UTF-8.There is no check that
targetstays inside the destination, so the symlink is created as-is.src/read/mod.rs: fn extract_internal(L396-406):src/read/mod.rs: fn make_symlink_impl(L57-90):Contradiction with the doc comment
The documentation comment of
extractatsrc/read/mod.rs: L266-269states the following.In reality, however, there is no validation when a symlink is created, and
canonicalizeis not used to validate symlink targets (canonicalizeis used only insrc/read/mod.rs: fn extract_internal(L357), to normalize the destination directory itself).Combining the above makes it possible to write outside the destination during extraction:
/…/outside(created without validation)<dest>/b/sub(lexically under dest, so it passes the check)Entries are processed in central directory order (
for i in 0..self.len()atsrc/read/mod.rs: L386), so the order above (b->a->a/pwned.txt) is required.Extraction then proceeds as follows:
b: without any validation, the symlink<dest>/b -> /…/outside, pointing outside the destination, is created.a: likewise without validation,<dest>/a -> <dest>/b/subis created.a/pwned.txt: whensafe_prepare_pathprocesses the componenta, it sees that<dest>/ais a symlink and validates its target (<dest>/b/sub). Lexically that target is under<dest>, so the check passes andoutpathbecomes<dest>/b/sub.b, an intermediate component of the target, is a symlink pointing outside is never re-validated.ais not the final component,make_writable_dir_allis called and creates<dest>/b/sub(which really is/…/outside/sub).File::createcreates<dest>/b/sub/pwned.txt; the kernel resolvesband writes to/…/outside/sub/pwned.txt.extract()returnsOk(()).PoC
The PoC below is written as a unit test.
Place it at
tests/poc_linux.rsand runcargo test --test poc_linux -- --nocaptureto reproduce.zip_slip_via_symlink: Write a new file outside the extraction destinationoverwrite_existing_ssh_authorized_keys: Overwrite an existing file ~/.ssh/authorized_keys outside the extraction destinationtests/poc_linux.rsWindows PoC (
tests/poc_win.rs)This is a test that verifies escape to a different drive / UNC share via a single symlink, specific to Windows.
By setting the extraction destination to a temporary directory on
C:and making the symlink target an absolute path on a different volume that shares the same tail as dest, it passes the lexical check insimplified_components(which discards thePrefix), and the drive/share substitution performed byoutpath.pushresults in a write outside ofdest.escape_via_second_drive: writes toE:\<same tail as dest>\evil\pwned.txton a separate drive (e.g.E:)escape_via_unc_share: writes to\\localhost\poc\<same tail as dest>\evil\pwned.txton a UNC share (e.g.\\localhost\poc\)Impact
With the privileges of the process that extracts an untrusted ZIP archive via
ZipArchive::extract/ZipArchive::extract_unwrapped_root_dir, an attacker can write a file with arbitrary contents to an arbitrary path outside the extraction directory.~/.ssh/authorized_keys,~/.bashrc,.git/hooks/*, CI configuration, or a service's binaries and configuration files may lead to remote code execution or privilege escalation. Destroying critical files can also cause denial of service.Ok(()), so the caller has no straightforward way to detect the attack.extract()explicitly states that symbolic links are only created and followed if the target is within the destination directory, so downstream applications that rely on this as a sandbox boundary are directly affected.Exploitation requires the absolute path of the destination to be known, but this is realistic in practice: CI workspaces,
/opt/<app>, a fixed Downloads directory, and so on.Severity
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
zip: legacy Shrink/Reduce/Implode decoder: denial of service from attacker-controlled ZIP64 uncompressed size
CVE-2026-115907 / GHSA-9hj8-73fh-9963
More information
Details
Summary
A malicious 149-byte ZIP archive can cause a denial of service in applications that enable
legacy-zipand useZipArchive::extract(). An attacker-controlled ZIP64 uncompressed size triggers acapacity overflowpanic 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:In version 8.6.0, this is located in
src/legacy/shrink.rs. The vulnerable legacy codecs are compiled when thelegacy-zipCargo feature is enabled.`self.uncompressed_size