Uncontrolled Archive Expansion

ID

php.expanding_archive_files_done_controlling

Severity

high

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Reliability

Language

Php

Tags

best-practice, reliability

Description

An archive is expanded, or a compressed payload inflated, with no limit on how much data comes out.

A few kilobytes of crafted zip or zlib data expand into gigabytes. With no ceiling, the process fills memory or the filesystem and the request — often the whole worker — dies, from an input the caller controls.

Two shapes are reported:

  • gzinflate(), gzuncompress(), gzdecode() and zlib_decode() called without their max_length argument, which defaults to "no limit";

  • extractTo() on a ZipArchive, Phar or PharData when no size check runs before it. extractTo() writes every entry in one go and reports the total only afterwards, by which point the disk is already full.

Rationale

public function unzip(string $archive, string $target): void
{
    $zip = new \ZipArchive();
    $zip->open($archive);
    $zip->extractTo($target);          // every entry, however large, straight to disk
    $zip->close();
}

public function inflate(string $payload): string
{
    return gzinflate($payload);        // unbounded output into memory
}

Remediation

Bound the decompression, or compute the expanded size before extracting.

public function inflate(string $payload): string
{
    return gzinflate($payload, self::MAX_BYTES);
}

public function unzip(string $archive, string $target): void
{
    $zip = new \ZipArchive();
    $zip->open($archive);

    $total = 0;
    for ($i = 0; $i < $zip->numFiles; $i++) {
        $total += $zip->statIndex($i)['size'];
    }

    if ($total > self::MAX_BYTES) {
        throw new \RuntimeException('archive too large once expanded');
    }

    $zip->extractTo($target);
    $zip->close();
}

The extractTo() finding is suppressed when a size enquiry runs earlier in the same scope: statIndex, statName, getSize, filesize, disk_free_space, memory_get_usage or a read of numFiles. Only statements that precede the extraction count — a check that runs after the archive is already on disk is not a check.

gzencode() and the other compressors are not reported, and neither is gzread(), which takes a length on every call.

© 2026 Xygeni. All rights reserved