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()andzlib_decode()called without theirmax_lengthargument, which defaults to "no limit"; -
extractTo()on aZipArchive,PharorPharDatawhen 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.