This rule is deprecated, and will eventually be removed.

Successful Zip Bomb attacks occur when an application expands untrusted archive files without controlling the size of the expanded data, which can lead to denial of service. A Zip bomb is usually a malicious archive file of a few kilobytes of compressed data but turned into gigabytes of uncompressed data. To achieve this extreme compression ratio, attackers will compress irrelevant data (eg: a long string of repeated bytes).

Why is this an issue?

Expanding archive files without controlling the size of the extracted data can lead to denial of service. A Zip bomb is a malicious archive of a few kilobytes of compressed data that expands into gigabytes of uncompressed data by compressing highly repetitive content. Applications that fail to validate the number of entries, total uncompressed size, or compression ratio of an archive are vulnerable to this attack.

What is the potential impact?

Denial of service

An attacker who can supply a malicious archive can exhaust the server’s disk space, memory, or CPU by triggering unbounded decompression. This can make the application completely unavailable to legitimate users and may require manual intervention to recover the affected system.

How to fix it in ZipArchive

Validate the number of entries, total uncompressed size, and compression ratio when extracting archive files.

Code examples

Noncompliant code example

$zip = new ZipArchive();
if ($zip->open($file) === true) {
    $zip->extractTo('.'); // Noncompliant
    $zip->close();
}

Compliant solution

define('MAX_FILES', 10000);
define('MAX_SIZE', 1000000000); // 1 GB
define('MAX_RATIO', 10);
define('READ_LENGTH', 1024);

$fileCount = 0;
$totalSize = 0;

$zip = new ZipArchive();
if ($zip->open($file) === true) {
    for ($i = 0; $i < $zip->numFiles; $i++) {
        $filename = $zip->getNameIndex($i);
        $stats = $zip->statIndex($i);

        if (strpos($filename, '../') !== false || substr($filename, 0, 1) === '/') {
            throw new Exception();
        }

        if (substr($filename, -1) !== '/') {
            $fileCount++;
            if ($fileCount > MAX_FILES) {
                // Reached max. number of files
                throw new Exception();
            }

            $fp = $zip->getStream($filename);
            $currentSize = 0;
            while (!feof($fp)) {
                $currentSize += READ_LENGTH;
                $totalSize += READ_LENGTH;

                if ($totalSize > MAX_SIZE) {
                    // Reached max. size
                    throw new Exception();
                }

                // Additional protection: check compression ratio
                if ($stats['comp_size'] > 0) {
                    $ratio = $currentSize / $stats['comp_size'];
                    if ($ratio > MAX_RATIO) {
                        // Reached max. compression ratio
                        throw new Exception();
                    }
                }

                file_put_contents($filename, fread($fp, READ_LENGTH), FILE_APPEND);
            }

            fclose($fp);
        } else {
            mkdir($filename);
        }
    }
    $zip->close();
}

How to fix it in Zip

Validate the number of entries, total uncompressed size, and compression ratio when extracting archive files.

Code examples

Noncompliant code example

$zip = zip_open($file);
while ($file = zip_read($zip)) {
    $filename = zip_entry_name($file);
    $size = zip_entry_filesize($file);

    if (substr($filename, -1) !== '/') {
        $content = zip_entry_read($file, zip_entry_filesize($file)); // Noncompliant - zip_entry_read() uses zip_entry_filesize()
        file_put_contents($filename, $content);
    } else {
        mkdir($filename);
    }
}
zip_close($zip);

Compliant solution

define('MAX_FILES', 10000);
define('MAX_SIZE', 1000000000); // 1 GB
define('MAX_RATIO', 10);
define('READ_LENGTH', 1024);

$fileCount = 0;
$totalSize = 0;

$zip = zip_open($file);
while ($file = zip_read($zip)) {
    $filename = zip_entry_name($file);

    if (strpos($filename, '../') !== false || substr($filename, 0, 1) === '/') {
        throw new Exception();
    }

    if (substr($filename, -1) !== '/') {
        $fileCount++;
        if ($fileCount > MAX_FILES) {
            // Reached max. number of files
            throw new Exception();
        }

        $currentSize = 0;
        while ($data = zip_entry_read($file, READ_LENGTH)) {
            $currentSize += READ_LENGTH;
            $totalSize += READ_LENGTH;

            if ($totalSize > MAX_SIZE) {
                // Reached max. size
                throw new Exception();
            }

            // Additional protection: check compression ratio
            if (zip_entry_compressedsize($file) > 0) {
                $ratio = $currentSize / zip_entry_compressedsize($file);
                if ($ratio > MAX_RATIO) {
                    // Reached max. compression ratio
                    throw new Exception();
                }
            }

            file_put_contents($filename, $data, FILE_APPEND);
        }
    } else {
        mkdir($filename);
    }
}
zip_close($zip);

Resources

Articles & blog posts

Standards