Summary
mz_zip_reader_entry_save_file() uses stat()-based existence checks before creating extracted files. Because stat() follows symlinks, dangling symlinks are incorrectly treated as nonexistent, bypassing the overwrite check. The subsequent file creation follows the symlink, allowing writes to an attacker-chosen location outside the extraction directory. This issue is independent of #1025 and remains exploitable even when an explicit -d/destination_dir is specified.
Root Cause
-
mz_os_file_exists() (mz_os_posix.c) calls stat(), which resolves symlinks. A dangling symlink causes stat() to fail with ENOENT, so the function treats the path as nonexistent even though the symlink itself exists.
-
mz_zip_reader_entry_save_file() (mz_zip_rw.c) uses this existence check to gate its overwrite/removal handling and subsequently calls mz_stream_os_open(path, MZ_OPEN_MODE_CREATE | MZ_OPEN_MODE_WRITE) on the same path.
-
mz_stream_os_open() ultimately calls fopen()/open() without an O_NOFOLLOW-style safeguard, so if path is a symlink, the open follows it and operates on the symlink target rather than the symlink itself.
Impact: A ZIP entry with a completely benign, non-traversing filename can still create or overwrite a file at an attacker-chosen location outside the intended extraction directory, provided a dangling symlink with the same name has been pre-planted at the destination. This issue remains exploitable even when an explicit -d/destination_dir is specified.
Reproduction
Pre-plant a dangling symlink at the destination path corresponding to the ZIP entry, then extract a normal single-entry ZIP archive (the entry name matches the symlink name, contains no path traversal, and is not a symlink entry) with an explicit -d/destination_dir.
PoC file: evil_1027.zip
Command:
minizip -x -o -d /tmp/poc ./evil.zip
Log
$ mkdir -p /tmp/dest /tmp/outside
$ ln -s /tmp/outside/pwned.txt /tmp/dest/link_name
$ stat -c '%A %N' /tmp/dest/link_name
lrwxrwxrwx '/tmp/dest/link_name' -> '/tmp/outside/pwned.txt'
$ ./minizip -x -o -d /tmp/dest /tmp/evil.zip
minizip-ng 4.2.2 - https://github.com/zlib-ng/minizip-ng
---------------------------------------------------
-x -o -d /tmp/dest /tmp/evil.zip
Archive /tmp/evil.zip
Extracting link_name
$ echo "--- dest ---"
ls -la /tmp/dest
--- dest ---
total 8
drwxr-xr-x 2 root root 4096 Jul 25 14:50 .
drwxrwxrwt 1 root root 4096 Jul 25 14:50 ..
lrwxrwxrwx 1 root root 22 Jul 25 14:50 link_name -> /tmp/outside/pwned.txt
echo "--- outside ---"
ls -la /tmp/outside
cat /tmp/outside/pwned.txt
--- outside ---
total 12
drwxr-xr-x 2 root root 4096 Jul 25 14:51 .
drwxrwxrwt 1 root root 4096 Jul 25 14:50 ..
-rw-r--r-- 1 root root 6 Nov 30 1979 pwned.txt
PWNED
Ref
GHSA-qcc4-p59m-p54m
Summary
mz_zip_reader_entry_save_file()usesstat()-based existence checks before creating extracted files. Becausestat()follows symlinks, dangling symlinks are incorrectly treated as nonexistent, bypassing the overwrite check. The subsequent file creation follows the symlink, allowing writes to an attacker-chosen location outside the extraction directory. This issue is independent of #1025 and remains exploitable even when an explicit-d/destination_diris specified.Root Cause
mz_os_file_exists()(mz_os_posix.c) callsstat(), which resolves symlinks. A dangling symlink causesstat()to fail withENOENT, so the function treats the path as nonexistent even though the symlink itself exists.mz_zip_reader_entry_save_file()(mz_zip_rw.c) uses this existence check to gate its overwrite/removal handling and subsequently callsmz_stream_os_open(path, MZ_OPEN_MODE_CREATE | MZ_OPEN_MODE_WRITE)on the same path.mz_stream_os_open()ultimately callsfopen()/open()without anO_NOFOLLOW-style safeguard, so ifpathis a symlink, the open follows it and operates on the symlink target rather than the symlink itself.Impact: A ZIP entry with a completely benign, non-traversing filename can still create or overwrite a file at an attacker-chosen location outside the intended extraction directory, provided a dangling symlink with the same name has been pre-planted at the destination. This issue remains exploitable even when an explicit
-d/destination_diris specified.Reproduction
Pre-plant a dangling symlink at the destination path corresponding to the ZIP entry, then extract a normal single-entry ZIP archive (the entry name matches the symlink name, contains no path traversal, and is not a symlink entry) with an explicit
-d/destination_dir.PoC file: evil_1027.zip
Command:
Log
Ref
GHSA-qcc4-p59m-p54m