Preventing Zip Bomb Attacks — Detection and Safe Extraction
A zip bomb is a malicious archive file designed to crash or disable the program or system that opens it. A 42KB ZIP file can expand to 4.5 petabytes. Here's how they work and how to protect your application.
What Is a Zip Bomb?
A zip bomb exploits the compression ratio of ZIP files. Highly repetitive data compresses extremely well:
- 42.zip — 42 KB compressed → 4.5 PB (4,500,000 GB) when fully extracted (nested ZIPs inside ZIPs)
- Flat zip bomb — A single ZIP with one file that's 10 GB of zeros → compresses to ~10 MB
- Quine zip — A ZIP file that contains itself recursively
When your server extracts a user-uploaded ZIP without size limits, a zip bomb can:
- Fill all disk space (denial of service)
- Exhaust memory (OOM kill)
- Overwhelm antivirus scanners
- Crash your application
How to Detect Zip Bombs
Method 1: Check Uncompressed Size Before Extracting
const AdmZip = require('adm-zip');
function isZipBomb(zipPath, maxUncompressedMB = 500) {
const zip = new AdmZip(zipPath);
const entries = zip.getEntries();
let totalUncompressed = 0;
for (const entry of entries) {
totalUncompressed += entry.header.size; // Uncompressed size from header
if (totalUncompressed > maxUncompressedMB * 1024 * 1024) {
return true; // Zip bomb detected
}
}
return false;
}
Limitation: The header size can be falsified. A sophisticated zip bomb reports a small uncompressed size in the header but expands to much more.
Method 2: Check Compression Ratio
Normal files compress 2-10x. A zip bomb compresses 1000x+.
function checkCompressionRatio(zipPath, maxRatio = 100) {
const zip = new AdmZip(zipPath);
const compressedSize = require('fs').statSync(zipPath).size;
let totalUncompressed = 0;
for (const entry of zip.getEntries()) {
totalUncompressed += entry.header.size;
}
const ratio = totalUncompressed / compressedSize;
if (ratio > maxRatio) {
throw new Error(`Suspicious compression ratio: ${ratio.toFixed(0)}:1`);
}
}
Method 3: Extract with Running Size Limit
The safest approach — stop extraction if the running total exceeds a threshold:
const { createWriteStream } = require('fs');
const yauzl = require('yauzl');
function safeExtract(zipPath, destDir, maxBytes = 500 * 1024 * 1024) {
return new Promise((resolve, reject) => {
let totalExtracted = 0;
yauzl.open(zipPath, { lazyEntries: true }, (err, zipFile) => {
if (err) return reject(err);
zipFile.readEntry();
zipFile.on('entry', (entry) => {
zipFile.openReadStream(entry, (err, stream) => {
if (err) return reject(err);
stream.on('data', (chunk) => {
totalExtracted += chunk.length;
if (totalExtracted > maxBytes) {
stream.destroy();
zipFile.close();
reject(new Error('Zip bomb detected: extraction size limit exceeded'));
}
});
const dest = createWriteStream(/* safe path */);
stream.pipe(dest);
dest.on('finish', () => zipFile.readEntry());
});
});
zipFile.on('end', resolve);
});
});
}
Method 4: Count Nesting Depth
Nested zip bombs (ZIP inside ZIP) are caught by limiting recursion:
# Python
import zipfile
def safe_extract(zip_path, dest, max_depth=3, current_depth=0):
if current_depth > max_depth:
raise ValueError("Maximum nesting depth exceeded — possible zip bomb")
with zipfile.ZipFile(zip_path) as zf:
for info in zf.infolist():
if info.file_size > 500 * 1024 * 1024: # 500MB limit per file
raise ValueError(f"File too large: {info.filename}")
zf.extract(info, dest)
# Check for nested ZIP
if info.filename.endswith('.zip'):
nested = os.path.join(dest, info.filename)
safe_extract(nested, dest, max_depth, current_depth + 1)
Defense Summary
| Defense | What It Catches | Bypassed By | |---------|----------------|-------------| | Header size check | Flat bombs | Falsified headers | | Compression ratio | High-ratio bombs | Normal-looking ratios | | Running extraction limit | All size bombs | Nothing (safest) | | Nesting depth limit | Recursive bombs | Flat bombs | | File count limit | Many-file bombs | Single large file |
Use all five together for comprehensive protection.
Test Your Protection
| Test | File | Expected | |------|------|----------| | Valid ZIP | sample-1mb.zip | Extracts normally | | Many files | sample-many-files.zip | 1000 files, should pass | | Nested ZIP | sample-nested.zip | Depth limit test | | Corrupted ZIP | sample-corrupt.zip | Graceful rejection | | Password ZIP | sample-with-password.zip | Password handling |
See also: ZIP Extraction Errors · Archive Formats Cheat Sheet