What is a ZIP bomb?
A ZIP bomb (also called a decompression bomb) is a maliciously crafted archive that appears small on disk but expands to an enormous size when extracted. The classic example is 42.zip: 42 kilobytes compressed, 4.5 petabytes decompressed.
If your application or antivirus scanner tries to extract the archive before scanning it, the process can exhaust disk space, RAM, or CPU — taking down your server or the scanner itself. There are two common variants:
- Recursive (nested) ZIP bombs. A chain of ZIP files, each containing another ZIP. Depth is limited so each level is small, but total expansion is enormous.
- Flat (non-recursive) ZIP bombs. A single ZIP whose entries overlap on the same compressed stream, achieving extreme compression ratios (up to 28 million to one). These evade many scanners that only check recursion depth.
ClamAV's built-in ZIP bomb detection
ClamAV has specific heuristics and signatures for ZIP bombs. When it detects one, it returns exit code 1 (malware found) with a signature name such as:
Heuristics.Zip.DoubleCompressionHeuristics.Zip.OverlappingFilesHeuristics.Zip.ExceedsMax
pompelmi maps exit code 1 to Verdict.Malicious, so a confirmed
ZIP bomb is returned as a malicious file. Your existing
Verdict.Malicious rejection logic handles it with no code changes.
ClamAV also enforces internal limits on maximum recursion depth, scan size,
and file count within archives. When an archive exceeds these limits, ClamAV
returns exit code 2, which pompelmi maps to
Verdict.ScanError.
Why ScanError is your second line of defense
A zip bomb sophisticated enough to evade ClamAV's heuristics may still cause
the scan to hit resource limits and terminate abnormally. pompelmi returns
Verdict.ScanError in that case.
Verdict.ScanError to storage. An attacker
can sometimes force a scan error intentionally — with an oversized or corrupted
archive — as a bypass technique. Treat ScanError as untrusted
and reject the file.
This is already the correct behaviour if you follow the standard pompelmi verdict-handling pattern:
const verdict = await scan(tmpPath);
switch (verdict) {
case Verdict.Clean:
// Proceed — file is safe
break;
case Verdict.Malicious:
// Confirmed threat (includes detected ZIP bombs)
fs.unlinkSync(tmpPath);
return res.status(400).json({ error: 'Malicious file rejected.' });
case Verdict.ScanError:
// Scan hit a limit or failed — treat as untrusted
fs.unlinkSync(tmpPath);
return res.status(422).json({ error: 'Scan incomplete. File rejected.' });
}
Pre-scan validation layer
The best defence against ZIP bombs is to validate the upload before the file reaches ClamAV, minimising the work the scanner has to do:
-
Enforce a strict file size limit. Multer's
limits.fileSizeoption rejects oversized uploads before they are written to disk. A 10 MB limit is reasonable for most applications. - Check the MIME type. If your application only needs PDFs and images, reject ZIP files entirely. Most applications do not need to accept arbitrary archive uploads.
-
Validate magic bytes. A ZIP file always starts with the
bytes
50 4B 03 04. Read the first four bytes and compare against your allowlist of permitted file types.
const MAGIC_BYTES = {
// Permitted types
pdf: [0x25, 0x50, 0x44, 0x46], // %PDF
png: [0x89, 0x50, 0x4E, 0x47], // PNG
jpeg: [0xFF, 0xD8, 0xFF], // JPEG
gif: [0x47, 0x49, 0x46, 0x38], // GIF8
// Dangerous types — reject before scanning
zip: [0x50, 0x4B, 0x03, 0x04], // PK\x03\x04
rar: [0x52, 0x61, 0x72, 0x21], // Rar!
};
function getFileMagic(filePath) {
const fd = require('fs').openSync(filePath, 'r');
const buf = Buffer.alloc(4);
require('fs').readSync(fd, buf, 0, 4, 0);
require('fs').closeSync(fd);
return buf;
}
function isArchive(filePath) {
const magic = getFileMagic(filePath);
return (
magic.slice(0, 4).equals(Buffer.from(MAGIC_BYTES.zip)) ||
magic.slice(0, 4).equals(Buffer.from(MAGIC_BYTES.rar))
);
}
Complete defended upload endpoint
const express = require('express');
const multer = require('multer');
const { scan, Verdict } = require('pompelmi');
const fs = require('fs');
const os = require('os');
const app = express();
const upload = multer({
dest: os.tmpdir(),
limits: {
fileSize: 10 * 1024 * 1024, // 10 MB — hard limit before disk write
files: 1,
},
});
// Reject archives by magic bytes before scanning
function isArchiveFile(filePath) {
const buf = Buffer.alloc(4);
const fd = fs.openSync(filePath, 'r');
fs.readSync(fd, buf, 0, 4, 0);
fs.closeSync(fd);
// ZIP: PK\x03\x04 RAR: Rar! 7z: 7z\xBC\xAF
return buf[0] === 0x50 && buf[1] === 0x4B ||
buf[0] === 0x52 && buf[1] === 0x61 ||
buf[0] === 0x37 && buf[1] === 0x7A;
}
app.post('/upload', upload.single('file'), async (req, res) => {
if (!req.file) return res.status(400).json({ error: 'No file provided.' });
const tmpPath = req.file.path;
try {
// Layer 1 — reject archives before ClamAV sees them
if (isArchiveFile(tmpPath)) {
return res.status(400).json({ error: 'Archive files are not accepted.' });
}
// Layer 2 — ClamAV scan
const verdict = await scan(tmpPath);
if (verdict === Verdict.Malicious) {
return res.status(400).json({ error: 'Malicious file rejected.' });
}
if (verdict === Verdict.ScanError) {
return res.status(422).json({ error: 'Scan incomplete. File rejected.' });
}
return res.json({ status: 'ok' });
} finally {
if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
}
});
OS-level resource limits
As a final layer of defence, constrain the resources available to the
clamscan process and to your Node.js process itself.
In a containerised environment (Docker, Kubernetes) set memory and CPU
limits on the container. If clamscan attempts to decompress a
ZIP bomb and exhausts memory, the container's OOM killer will terminate the
process, and pompelmi will throw rather than hanging indefinitely. Handle
the thrown error in your catch block:
try {
const verdict = await scan(tmpPath);
// ...
} catch (err) {
// Catches ENOENT (ClamAV not installed), OOM kill (SIGKILL), etc.
if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
return res.status(500).json({ error: 'Scan failed: ' + err.message });
}
In a Kubernetes Deployment, the resource limits you set in the pod spec provide this guarantee automatically. See Setting up pompelmi with ClamAV on Kubernetes for a full example with memory and CPU limits.
Next steps
- Want the complete file upload security picture beyond ZIP bombs? Read the Node.js file upload security checklist.
- Worried about macro-laden Office files? See Scanning Excel/CSV files for malicious macros.
- Handling encrypted archives? See How to handle encrypted/password-protected files during scan.