From 18599cca01e1e88877fcad9285e134e5d9aa61fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eze=F0=9F=91=BD?= Date: Wed, 19 Aug 2026 04:48:08 -0300 Subject: [PATCH] test: rebase PR #188 onto master; catalog regenerated --- docs/VULNERABILITY_CATALOG.md | 9 +++-- vulns/VULNERABILITY_CATALOG.json | 32 ++++++++++++++-- vulns/python/unbounded-zip-bomb.py | 60 ++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 8 deletions(-) create mode 100644 vulns/python/unbounded-zip-bomb.py diff --git a/docs/VULNERABILITY_CATALOG.md b/docs/VULNERABILITY_CATALOG.md index 33039de..4f7ed28 100644 --- a/docs/VULNERABILITY_CATALOG.md +++ b/docs/VULNERABILITY_CATALOG.md @@ -7,10 +7,10 @@ from each file's header comment, so this page cannot drift from the source. ## Totals -- **Test cases:** 116 -- **Expected detections:** 116 -- **`VULNERABLE:` markers:** 215 (individual lines a scanner should flag) -- **`SAFE:` markers:** 131 (lines a scanner must not flag — the false-positive control group) +- **Test cases:** 117 +- **Expected detections:** 117 +- **`VULNERABLE:` markers:** 217 (individual lines a scanner should flag) +- **`SAFE:` markers:** 132 (lines a scanner must not flag — the false-positive control group) - **Languages:** 8 — dotenv, go, java, javascript, json, python, ruby, text - **CWE categories:** 84 — CWE-20, CWE-22, CWE-78, CWE-79, CWE-89, CWE-90, CWE-94, CWE-95, CWE-113, CWE-117, CWE-129, CWE-190, CWE-201, CWE-203, CWE-208, CWE-209, CWE-256, CWE-287, CWE-288, CWE-291, CWE-295, CWE-306, CWE-307, CWE-319, CWE-321, CWE-326, CWE-327, CWE-329, CWE-330, CWE-338, CWE-345, CWE-346, CWE-347, CWE-352, CWE-362, CWE-377, CWE-384, CWE-400, CWE-434, CWE-441, CWE-460, CWE-472, CWE-475, CWE-480, CWE-488, CWE-489, CWE-502, CWE-506, CWE-509, CWE-512, CWE-521, CWE-525, CWE-532, CWE-598, CWE-601, CWE-602, CWE-611, CWE-613, CWE-614, CWE-620, CWE-639, CWE-640, CWE-643, CWE-681, CWE-693, CWE-732, CWE-759, CWE-776, CWE-798, CWE-835, CWE-862, CWE-863, CWE-915, CWE-916, CWE-918, CWE-922, CWE-942, CWE-943, CWE-1021, CWE-1236, CWE-1321, CWE-1333, CWE-1336, CWE-1357 @@ -137,6 +137,7 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | Sensitive information exposure via raw stack trace in HTTP response | [`stack-trace-exposure.py`](../vulns/python/stack-trace-exposure.py) | CWE-209 | medium | yes | 2 vuln / 1 safe | | TLS certificate validation disabled on HTTP client requests | [`tls-verify-disabled.py`](../vulns/python/tls-verify-disabled.py) | CWE-295 | high | yes | 4 vuln / 1 safe | | TOCTOU race condition in file access | [`toctou-race-condition.py`](../vulns/python/toctou-race-condition.py) | CWE-362 | medium | yes | 3 vuln / 2 safe | +| Uncontrolled resource consumption via zip decompression bomb | [`unbounded-zip-bomb.py`](../vulns/python/unbounded-zip-bomb.py) | CWE-400 | medium | yes | 2 vuln / 1 safe | | Unrestricted upload of a dangerous executable file type | [`unrestricted-file-upload.py`](../vulns/python/unrestricted-file-upload.py) | CWE-434 | high | yes | 2 vuln / 1 safe | | Password change accepted without verifying the current password | [`unverified-password-change.py`](../vulns/python/unverified-password-change.py) | CWE-620 | high | yes | 2 vuln / 1 safe | | Weak cryptographic algorithms for integrity and confidentiality | [`weak-crypto-md5.py`](../vulns/python/weak-crypto-md5.py) | CWE-327 | high | yes | 5 vuln / 2 safe | diff --git a/vulns/VULNERABILITY_CATALOG.json b/vulns/VULNERABILITY_CATALOG.json index 6f8dbe5..5f7f282 100644 --- a/vulns/VULNERABILITY_CATALOG.json +++ b/vulns/VULNERABILITY_CATALOG.json @@ -2,10 +2,10 @@ "schema": "threatcrush-testbed-catalog/1", "note": "Generated by scripts/generate-catalog.py \u2014 do not edit by hand.", "totals": { - "test_cases": 116, - "expected_detections": 116, - "vulnerable_markers": 215, - "safe_markers": 131, + "test_cases": 117, + "expected_detections": 117, + "vulnerable_markers": 217, + "safe_markers": 132, "languages": [ "dotenv", "go", @@ -2402,6 +2402,30 @@ 74 ] }, + { + "id": "py-unbounded-zip-bomb", + "file": "vulns/python/unbounded-zip-bomb.py", + "title": "Uncontrolled resource consumption via zip decompression bomb", + "category": "python", + "language": "python", + "cwe": "CWE-400", + "cwes": [ + "CWE-400" + ], + "severity": "medium", + "expected_detection": true, + "description": "A zip archive supplied by an attacker is extracted without", + "detection_target": "zipfile.ZipFile.extractall() / extract() called without", + "safe_guard": "All extraction wrapped in `if False:` (unreachable dead code).", + "attribution": "line", + "vulnerable_lines": [ + 30, + 38 + ], + "safe_lines": [ + 50 + ] + }, { "id": "py-unrestricted-file-upload", "file": "vulns/python/unrestricted-file-upload.py", diff --git a/vulns/python/unbounded-zip-bomb.py b/vulns/python/unbounded-zip-bomb.py new file mode 100644 index 0000000..2a87c71 --- /dev/null +++ b/vulns/python/unbounded-zip-bomb.py @@ -0,0 +1,60 @@ +""" +@id py-unbounded-zip-bomb +@test-case Uncontrolled resource consumption via zip decompression bomb +@cwe CWE-400 +@severity medium +@language python +@expected-detection true +@description A zip archive supplied by an attacker is extracted without + validating the uncompressed size, compression ratio, or entry + count. A small malicious archive (zip bomb) can expand to + gigabytes, exhausting disk and memory. The safe counterpart + enforces per-entry and total size limits before extraction. +@safe-guard All extraction wrapped in `if False:` (unreachable dead code). + No archive is read from disk or network; the ZipFile object is + constructed from a synthetic bytes literal. +@detection-target zipfile.ZipFile.extractall() / extract() called without + prior checks on ZipInfo.file_size, compress_size, or + total uncompressed bytes; also shutil.unpack_archive + without limits. + +NEVER RUN IN PRODUCTION - intentional test case for scanner validation. +""" + +import zipfile +import io + + +def extract_zip_unchecked(zip_bytes: bytes, dest: str) -> None: + if False: + # VULNERABLE: CWE-400 - no size/ratio validation + with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: + zf.extractall(dest) + return None + + +def extract_zip_single_unchecked(zip_bytes: bytes, dest: str, member: str) -> None: + if False: + # VULNERABLE: CWE-400 - single entry, still no bounds + with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: + zf.extract(member, dest) + return None + + +def extract_zip_safe(zip_bytes: bytes, dest: str, max_total: int = 10_000_000, max_ratio: float = 100.0) -> None: + """Safe counterpart - the scanner should NOT flag this. + + @expected-detection false + """ + if False: + # SAFE: enforce total uncompressed size and compression ratio limits + total = 0 + with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf: + for info in zf.infolist(): + if info.compress_size > 0 and info.file_size / info.compress_size > max_ratio: + raise ValueError("suspicious compression ratio") + total += info.file_size + if total > max_total: + raise ValueError("uncompressed size exceeds limit") + zf.extractall(dest) + return None \ No newline at end of file