Improper Handling of Highly Compressed Data (Data Amplification)

Description

Improper Handling of Highly Compressed Data is a vulnerability that occurs when a product fails to properly manage compressed inputs with exceptionally high compression ratios that generate disproportionately large outputs upon decompression. A classic example is the "decompression bomb" (or "zip bomb")—a compact file that expands to enormous proportions when decompressed, potentially consuming massive system resources. This vulnerability can also manifest through XML bombs using recursive entity expansion.

Risk

Decompression bombs enable denial of service attacks with minimal attacker bandwidth. A 42KB zip file can expand to 4.5 petabytes. XML bombs can expand a few kilobytes of XML into gigabytes of memory. These attacks are particularly effective against automated systems that process uploaded files, email attachments, or API payloads without checking decompression ratios. The attacks can crash servers, fill disks, exhaust memory, and cause prolonged service outages. Nested archives can bypass simple size checks by hiding the bomb within multiple compression layers.

Solution

Set limits on decompression output size relative to input size. Implement streaming decompression that tracks output size incrementally. Abort decompression when output exceeds threshold. Disable entity expansion in XML parsers or set strict limits. Scan compressed files for nested archives and apply limits at each level. Use libraries that support decompression limits. Monitor resource consumption during decompression operations. Consider using sandboxed environments for processing untrusted compressed data.

Common Consequences

ImpactDetails
AvailabilityScope: Availability

CPU and memory resources deplete rapidly. System performance degrades significantly. Complete system crashes or service interruptions may occur. Denial of Service through resource exhaustion.

Example Code

Vulnerable Code

# Vulnerable: No decompression limit
import zipfile
import io

def vulnerable_extract_zip(zip_data):
    # Vulnerable: Extracts without checking decompression ratio
    with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
        for member in zf.namelist():
            # Vulnerable: No size limit - zip bomb can exhaust disk/memory
            zf.extract(member, "/tmp/extracted")

# Vulnerable: Reading entire decompressed content into memory
def vulnerable_read_gzip(gzip_data):
    import gzip
    # Vulnerable: Decompresses entire file into memory
    return gzip.decompress(gzip_data)
// Vulnerable: No decompression ratio check
import java.util.zip.*;

public class VulnerableDecompressor {

    public void extractZip(InputStream zipStream, String destDir)
            throws IOException {
        try (ZipInputStream zis = new ZipInputStream(zipStream)) {
            ZipEntry entry;
            while ((entry = zis.getNextEntry()) != null) {
                // Vulnerable: No size check before extraction
                File file = new File(destDir, entry.getName());

                try (FileOutputStream fos = new FileOutputStream(file)) {
                    byte[] buffer = new byte[8192];
                    int len;
                    // Vulnerable: Extracts unlimited data
                    while ((len = zis.read(buffer)) > 0) {
                        fos.write(buffer, 0, len);
                    }
                }
            }
        }
    }
}
<!-- Vulnerable: XML bomb using entity expansion -->
<?xml version="1.0"?>
<!DOCTYPE bomb [
  <!ENTITY a "AAAAAAAAAA">
  <!ENTITY b "&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;">
  <!ENTITY c "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">
  <!ENTITY d "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;">
  <!ENTITY e "&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;">
  <!ENTITY f "&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;">
  <!-- Each level multiplies by 10: 10^6 = 1,000,000 'A' characters -->
]>
<bomb>&f;</bomb>

Fixed Code

# Fixed: Implement decompression limits
import zipfile
import io

MAX_DECOMPRESSION_RATIO = 100  # Max 100x expansion
MAX_OUTPUT_SIZE = 100 * 1024 * 1024  # 100MB max total

def secure_extract_zip(zip_data, dest_dir):
    input_size = len(zip_data)
    total_extracted = 0

    with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
        for member in zf.infolist():
            # Fixed: Check uncompressed size before extraction
            if member.file_size > MAX_OUTPUT_SIZE:
                raise ValueError(f"File {member.filename} too large: {member.file_size}")

            # Fixed: Check decompression ratio
            if member.compress_size > 0:
                ratio = member.file_size / member.compress_size
                if ratio > MAX_DECOMPRESSION_RATIO:
                    raise ValueError(f"Suspicious compression ratio: {ratio}")

            total_extracted += member.file_size
            if total_extracted > MAX_OUTPUT_SIZE:
                raise ValueError("Total extraction size exceeded")

            # Fixed: Extract with size verified
            zf.extract(member, dest_dir)

# Fixed: Streaming decompression with limit
def secure_read_gzip(gzip_data, max_size=10*1024*1024):
    import gzip
    import io

    result = io.BytesIO()
    bytes_read = 0

    with gzip.GzipFile(fileobj=io.BytesIO(gzip_data)) as gz:
        while True:
            chunk = gz.read(8192)
            if not chunk:
                break
            bytes_read += len(chunk)
            # Fixed: Abort if limit exceeded
            if bytes_read > max_size:
                raise ValueError("Decompressed size exceeds limit")
            result.write(chunk)

    return result.getvalue()
// Fixed: Check decompression ratio and set limits
import java.util.zip.*;

public class SecureDecompressor {
    private static final long MAX_OUTPUT_SIZE = 100 * 1024 * 1024;  // 100MB
    private static final double MAX_RATIO = 100.0;

    public void extractZip(InputStream zipStream, String destDir)
            throws IOException {
        long totalExtracted = 0;

        try (ZipInputStream zis = new ZipInputStream(zipStream)) {
            ZipEntry entry;
            while ((entry = zis.getNextEntry()) != null) {
                // Fixed: Check declared size
                if (entry.getSize() > MAX_OUTPUT_SIZE) {
                    throw new SecurityException("Entry too large: " + entry.getName());
                }

                // Fixed: Prevent path traversal
                File file = new File(destDir, entry.getName());
                if (!file.getCanonicalPath().startsWith(
                        new File(destDir).getCanonicalPath())) {
                    throw new SecurityException("Path traversal attempt");
                }

                long entrySize = 0;
                try (FileOutputStream fos = new FileOutputStream(file)) {
                    byte[] buffer = new byte[8192];
                    int len;
                    while ((len = zis.read(buffer)) > 0) {
                        entrySize += len;
                        totalExtracted += len;

                        // Fixed: Check limits during extraction
                        if (entrySize > MAX_OUTPUT_SIZE ||
                            totalExtracted > MAX_OUTPUT_SIZE * 10) {
                            file.delete();
                            throw new SecurityException("Decompression limit exceeded");
                        }

                        fos.write(buffer, 0, len);
                    }
                }

                // Fixed: Check actual ratio after extraction
                if (entry.getCompressedSize() > 0) {
                    double ratio = (double) entrySize / entry.getCompressedSize();
                    if (ratio > MAX_RATIO) {
                        file.delete();
                        throw new SecurityException("Suspicious compression ratio");
                    }
                }
            }
        }
    }
}
# Fixed: Disable XML entity expansion
import defusedxml.ElementTree as ET

def secure_parse_xml(xml_string):
    # Fixed: defusedxml prevents entity expansion attacks
    return ET.fromstring(xml_string)

# Alternative: Configure lxml to disable entities
from lxml import etree

def secure_parse_xml_lxml(xml_string):
    parser = etree.XMLParser(
        resolve_entities=False,  # Disable entity resolution
        no_network=True,         # Disable network access
        dtd_validation=False,    # Disable DTD
        load_dtd=False
    )
    return etree.fromstring(xml_string.encode(), parser)

CVE Examples

  • CVE-2009-1955 — XML bomb vulnerability in Apache APR-util library.
  • CVE-2003-1564 — Parsing library susceptible to XML bomb attacks.
  • CVE-2019-9674 — Python zipfile module vulnerable to zip bombs.

References

  1. MITRE Corporation. "CWE-409: Improper Handling of Highly Compressed Data (Data Amplification)." https://cwe.mitre.org/data/definitions/409.html
  2. AeroSpike. "Zip Bomb" https://www.bamsoftware.com/hacks/zipbomb/