high · 7.4 Patched

zip: Arbitrary file write outside the extraction directory, because ZipArchive::extract validates symlink targets lexically.

CVE-2026-115908 · GHSA-hjqx-j4rw-g7xv · published 29 Oct 2026

How serious is it

  • Severity: high, CVSS score 7.4 out of 10.
  • No exploit prediction (EPSS) score yet.
  • Not on the CISA list of exploited vulnerabilities.
  • No public exploit in Exploit-DB.

Is your app affected by CVE-2026-115908?

Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.

Who is affected

  • zip (crates.io) >= 7.2.0, < 9.0.0

How to check

  1. Run cargo tree -i zip in your project to see the installed version, including copies pulled in by other packages.
  2. Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.

How to fix

Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
  1. Upgrade zip to 9.0.0 or later, commit the lockfile and redeploy.
  2. If a vulnerable version was live, check your logs and rotate the secrets that server could read.

The advisory

### Summary - **Verified version:** zip `9.0.0-pre3` - **Affected versions:** `7.2.0` and later - **Precondition:** The attacker knows (or can guess) the absolute path of the extraction destination The 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. > **Impact extends further on Windows (verified on real hardware for both the separate-drive and UNC-share cases)**: > Because `simplified_components` discards the `Prefix` component, paths like `D:\<same tail as dest>` or a UNC path `\\attacker\share\<same tail>` pass the `starts_with` check. > As a result, `outpath.push` gets replaced onto a different drive/remote share, so **a single symlink is enough — no intermediate symlink chain is required**. > That said, creating a symlink on Windows requires `SeCreateSymbolicLinkPrivilege` (administrator rights or Developer Mode enabled). > **Real-hardware verification (Windows 10 VM (19045.2604), `tests/poc_win.rs`)**: > With the extraction target set to a temporary directory under `C:`, both of the following paths returned `Ok(())` from `extract()` and successfully wrote outside the extraction target. > In both cases `extract()` returned no error (`Ok(())`), and it was confirmed that the contents of `pwned.txt` matched the attacker-specified value. ### Relationship to CVE-2025-29787 This is **not a duplicate** of the existing [CVE-2025-29787](https://github.com/zip-rs/zip2/security/advisories/GHSA-94vh-gphv-8pm8) (2025-03); rather, **the protection introduced for that CVE was subsequently lost**. #### Timeline (git history, UTC) | commit | Date | Description | | ---------- | ---------- | ---------------------------------------------------------------------------------------------------------------- | | `d4539d6b` | 2025-02-28 | Initial fix for CVE-2025-29787. Canonicalizes output paths and symlink targets and verifies they descend from the destination | | `0199ac2c` | 2025-03-15 | **Before the v2.3.0 release**, the above is replaced with a lexical check based on `simplified_components` | | `v2.3.0` | 2025-03-16 | Released as the CVE fix. **What actually shipped is the lexical check** | | `d5cf4b55` | 2026-01-18 | Changes how `simplified_components` handles absolute paths, **from rejecting them to silently stripping them** (released in `v7.2.0`) | #### Why v2.3.0 was not affected, and how the protection was lost The `safe_prepare_path` in v2.3.0 performs essentially the same lexical check as the current code. However, `simplified_components` at 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. ```rust // v2.3.0: reject absolute paths Component::Prefix(_) | Component::RootDir => return None, // Current: ignore the fact that the path is absolute Utf8WindowsComponent::Prefix(_) | Utf8WindowsComponent::RootDir => (), ``` `d5cf4b55` was 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`): | Payload / condition | Result | | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | | Original PoC for CVE-2025-29787 (relative symlink `parent -> ../`) | **Blocked** with `Err(InvalidArchive("Invalid symlink target path"))` (the CVE fix itself is effective) | | PoC in this report (two-stage symlink using absolute paths) | **Succeeds** with `Ok(())`, writing outside the destination | | PoC in this report, with the `d5cf4b55` change reverted to `return None` | **Blocked** with `Err(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 - **Stricter precondition:** This issue requires the absolute path of the destination to be known (`AC:H`). The CVE worked with a relative `../` and had no such precondition. - **Narrower API surface:** This issue affects only `ZipArchive::extract` (and `extract_unwrapped_root_dir`). `ZipStreamReader::extract` is not affected, because `external_attributes` in the local header is `0` and therefore `is_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-345` is lexical. **`src/read/zipfile.rs: fn safe_prepare_path` (L316-321):** ```rust if !crate::path::simplified_components(&target) .ok_or(invalid!("Invalid symlink target path"))? .starts_with( &crate::path::simplified_components(base_path) .ok_or(invalid!("Invalid base path"))?, ) ``` `simplified_components` discards 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 S…

Check your whole app

Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.

References

Merged from GitHub Advisory Database. Last updated 30 Oct 2026. All vulnerabilities · RSS · JSON