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).
Ask Yourself Whether
Archives to expand are untrusted and:
- There is no validation of the number of entries in the archive.
- There is no validation of the total size of the uncompressed data.
- There is no validation of the ratio between the compressed and uncompressed archive entry.
There is a risk if you answered yes to any of those questions.
Recommended Secure Coding Practices
- Define and control the ratio between compressed and uncompressed data, in general the data compression ratio for most of the legit archives is
1 to 3.
- Define and control the threshold for maximum total size of the uncompressed data.
- Count the number of file entries extracted from the archive and abort the extraction if their number is greater than a predefined threshold, in
particular it’s not recommended to recursively expand archives (an entry of an archive could be also an archive).
Sensitive Code Example
For ZipArchive module:
$zip = new ZipArchive();
if ($zip->open($file) === true) {
$zip->extractTo('.'); // Sensitive
$zip->close();
}
For Zip module:
$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)); // Sensitive - zip_entry_read() uses zip_entry_filesize()
file_put_contents($filename, $content);
} else {
mkdir($filename);
}
}
zip_close($zip);
Compliant Solution
For ZipArchive module:
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); // Compliant
$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();
}
For Zip module:
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)) { // Compliant
$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);
See