ASVS 5.0 V5.2.3 (level 2) requires that the application checks compressed files, such as zip, gz, docx and odt, against a maximum allowed uncompressed size and a maximum number of files, before uncompressing the file.
Note the inclusion of docx and odt. Office documents are zip files with a different name, so a feature that "only accepts Word files" is also an archive feature.
Why it matters
A zip bomb is a small upload that expands enormously. A file of a few hundred kilobytes can hold gigabytes of repeated data, and nested archives multiply the effect. If your code extracts first and asks questions later, the disk fills, memory runs out, or the process spends minutes decompressing. Many files at once can also exhaust inodes or file handles even if the total size is small.
The 5.2.1 size cap on the upload itself does not help here, since the upload is tiny. The limit that matters is on what comes out.
How to implement it
Read the archive index first, which does not require decompressing, and reject if the declared totals are over budget. Then enforce the budget again while extracting, because the declared sizes in an archive header can be false.
Python version using zipfile:
import zipfile
MAX_FILES = 200
MAX_TOTAL = 50 * 1024 * 1024 # 50 MB unpacked
MAX_ENTRY = 10 * 1024 * 1024
MAX_RATIO = 100
def safe_entries(path):
with zipfile.ZipFile(path) as z:
infos = z.infolist()
if len(infos) > MAX_FILES:
raise ValueError('too many files')
total = sum(i.file_size for i in infos)
if total > MAX_TOTAL:
raise ValueError('archive too large when unpacked')
for i in infos:
if i.file_size > MAX_ENTRY:
raise ValueError('entry too large')
if i.compress_size and i.file_size / i.compress_size > MAX_RATIO:
raise ValueError('suspicious compression ratio')
return infos
When extracting, read in chunks and keep a running count of real bytes, stopping at the limit no matter what the header said. Do not recurse into nested archives, or limit nesting depth to one. Extract into a temporary directory with its own disk quota, and run it with a time limit. Treat docx and odt the same way, and prefer a document parsing library that exposes its own limits.
How to test it
Create archives that break each rule and confirm each is rejected before extraction begins.
# 1 GB of zeros compresses to about 1 MB
head -c 1073741824 /dev/zero | zip -q bomb.zip -
# many tiny files
mkdir many && for i in $(seq 1 5000); do : > many/f$i; done && zip -qr many.zip many
Upload both and expect a 4xx response and no leftover files in the temp directory. Test a file with a false header size by crafting an archive whose entry claims to be 1 KB but holds more, and confirm the streaming counter stops it. Rename bomb.zip to bomb.docx and check it is treated the same. During every test, watch disk, memory and CPU on the server.
Common mistakes
- Checking only the upload size, which is small for a bomb.
- Trusting declared sizes in the header and not counting the bytes actually produced.
- Allowing nested archives with no depth limit.
- Forgetting docx, xlsx, odt and jar files, which are all archives.
- Extracting into a shared disk with no quota, so one upload fills it for everyone.
- Unlimited file count, which exhausts inodes.
Quick check
Why is the uploaded file size not enough to stop a zip bomb?
The upload is tiny, the damage comes from the expanded output. Limit unpacked size and entry count before extraction and keep counting during it.
Which common file types are really archives?
docx, xlsx, pptx, odt and jar files are zip containers, so the same limits apply.
Sources
- OWASP Application Security Verification Standard, requirement V5.2.3 (level 2)
- OWASP File Upload Cheat Sheet