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
- Run
cargo tree -i zipin your project to see the installed version, including copies pulled in by other packages. - 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.
- Upgrade
zipto 9.0.0 or later, commit the lockfile and redeploy. - 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
- github.com/zip-rs/zip2/security/advisories/GHSA-hjqx-j4rw-g7xv
- nvd.nist.gov/vuln/detail/CVE-2026-115908
- github.com/zip-rs/zip2/commit/09c030eab56e8db8fb3c1c33f554407b30bad8fa
- github.com/zip-rs/zip2/releases/tag/v9.0.0
- github.com/advisories/GHSA-hjqx-j4rw-g7xv
Merged from GitHub Advisory Database. Last updated 30 Oct 2026. All vulnerabilities · RSS · JSON