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).
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.
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.
Validate the number of entries and total uncompressed size when extracting archive files using the filter option.
const tar = require('tar');
tar.x({ // Noncompliant
file: 'foo.tar.gz'
});
const tar = require('tar');
const MAX_FILES = 10000;
const MAX_SIZE = 1000000000; // 1 GB
let fileCount = 0;
let totalSize = 0;
tar.x({
file: 'foo.tar.gz',
filter: (path, entry) => {
fileCount++;
if (fileCount > MAX_FILES) {
throw 'Reached max. number of files';
}
totalSize += entry.size;
if (totalSize > MAX_SIZE) {
throw 'Reached max. size';
}
return true;
}
});
Validate the number of entries, total uncompressed size, and compression ratio when extracting archive files.
const AdmZip = require('adm-zip');
let zip = new AdmZip("./foo.zip");
zip.extractAllTo("."); // Noncompliant
const AdmZip = require('adm-zip');
const MAX_FILES = 10000;
const MAX_SIZE = 1000000000; // 1 GB
const THRESHOLD_RATIO = 10;
let fileCount = 0;
let totalSize = 0;
let zip = new AdmZip("./foo.zip");
let zipEntries = zip.getEntries();
zipEntries.forEach(function(zipEntry) {
fileCount++;
if (fileCount > MAX_FILES) {
throw 'Reached max. number of files';
}
let entrySize = zipEntry.getData().length;
totalSize += entrySize;
if (totalSize > MAX_SIZE) {
throw 'Reached max. size';
}
let compressionRatio = entrySize / zipEntry.header.compressedSize;
if (compressionRatio > THRESHOLD_RATIO) {
throw 'Reached max. compression ratio';
}
if (!zipEntry.isDirectory) {
zip.extractEntryTo(zipEntry.entryName, ".");
}
});
Validate the number of file entries when extracting archive files.
Be aware that JSZip’s async-only decompression API makes per-chunk size enforcement impossible: async('nodebuffer') fully
decompresses each entry before any check runs, and a throw inside the .then() callback silently rejects the Promise without
halting iteration.
Only entry-count protection is reliable with this API.
It is up to the developer to decide if the implementation is secure.
const fs = require("fs");
const JSZip = require("jszip");
fs.readFile("foo.zip", function(err, data) {
if (err) throw err;
JSZip.loadAsync(data).then(function (zip) { // Noncompliant
zip.forEach(function (relativePath, zipEntry) {
if (!zip.file(zipEntry.name)) {
fs.mkdirSync(zipEntry.name);
} else {
zip.file(zipEntry.name).async('nodebuffer').then(function (content) {
fs.writeFileSync(zipEntry.name, content);
});
}
});
});
});
const fs = require("fs");
const pathmodule = require("path");
const JSZip = require("jszip");
const MAX_FILES = 10000;
let fileCount = 0;
let targetDirectory = __dirname + '/archive_tmp';
fs.readFile("foo.zip", function(err, data) {
if (err) throw err;
JSZip.loadAsync(data).then(function (zip) {
zip.forEach(function (relativePath, zipEntry) {
fileCount++;
if (fileCount > MAX_FILES) {
throw 'Reached max. number of files';
}
// Prevent ZipSlip path traversal (S6096)
const resolvedPath = pathmodule.join(targetDirectory, zipEntry.name);
if (!resolvedPath.startsWith(targetDirectory)) {
throw 'Path traversal detected';
}
if (!zip.file(zipEntry.name)) {
fs.mkdirSync(resolvedPath);
} else {
zip.file(zipEntry.name).async('nodebuffer').then(function (content) {
fs.writeFileSync(resolvedPath, content);
});
}
});
});
});
Validate the number of entries, total uncompressed size, and compression ratio when extracting archive files.
Be aware that due to the similar structure of noncompliant and compliant code the issue will be raised in both cases. It is up to the developer to decide if the implementation is secure.
const yauzl = require('yauzl');
yauzl.open('foo.zip', function (err, zipfile) {
if (err) throw err;
zipfile.on("entry", function(entry) {
zipfile.openReadStream(entry, function(err, readStream) { // Noncompliant
if (err) throw err;
// TODO: extract
});
});
});
const yauzl = require('yauzl');
const MAX_FILES = 10000;
const MAX_SIZE = 1000000000; // 1 GB
const THRESHOLD_RATIO = 10;
yauzl.open('foo.zip', function (err, zipfile) {
if (err) throw err;
let fileCount = 0;
let totalSize = 0;
zipfile.on("entry", function(entry) {
fileCount++;
if (fileCount > MAX_FILES) {
throw 'Reached max. number of files';
}
// The uncompressedSize comes from the zip headers, so it might not be trustworthy.
// Alternatively, calculate the size from the readStream.
let entrySize = entry.uncompressedSize;
totalSize += entrySize;
if (totalSize > MAX_SIZE) {
throw 'Reached max. size';
}
if (entry.compressedSize > 0) {
let compressionRatio = entrySize / entry.compressedSize;
if (compressionRatio > THRESHOLD_RATIO) {
throw 'Reached max. compression ratio';
}
}
zipfile.openReadStream(entry, function(err, readStream) {
if (err) throw err;
// TODO: extract
});
});
});
Validate the number of entries, total uncompressed size, and compression ratio when extracting archive files using the onEntry
callback.
const extract = require('extract-zip')
async function main() {
let target = __dirname + '/test';
await extract('test.zip', { dir: target }); // Noncompliant
}
main();
const extract = require('extract-zip')
const MAX_FILES = 10000;
const MAX_SIZE = 1000000000; // 1 GB
const THRESHOLD_RATIO = 10;
async function main() {
let fileCount = 0;
let totalSize = 0;
let target = __dirname + '/foo';
await extract('foo.zip', {
dir: target,
onEntry: function(entry, zipfile) {
fileCount++;
if (fileCount > MAX_FILES) {
throw 'Reached max. number of files';
}
// The uncompressedSize comes from the zip headers, so it might not be trustworthy.
// Alternatively, calculate the size from the readStream.
let entrySize = entry.uncompressedSize;
totalSize += entrySize;
if (totalSize > MAX_SIZE) {
throw 'Reached max. size';
}
if (entry.compressedSize > 0) {
let compressionRatio = entrySize / entry.compressedSize;
if (compressionRatio > THRESHOLD_RATIO) {
throw 'Reached max. compression ratio';
}
}
}
});
}
main();