From e3b3af2332c6046d3c015adfed83d2e019c172b8 Mon Sep 17 00:00:00 2001 From: CSTRSK Date: Sun, 9 Aug 2026 11:15:10 +0000 Subject: [PATCH] test-cases: add CWE-300 (python) --- docs/VULNERABILITY_CATALOG.md | 11 ++++---- vulns/VULNERABILITY_CATALOG.json | 32 ++++++++++++++++++++--- vulns/python/cwe-300-python.py | 45 ++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 vulns/python/cwe-300-python.py diff --git a/docs/VULNERABILITY_CATALOG.md b/docs/VULNERABILITY_CATALOG.md index 5763d34..b2663e4 100644 --- a/docs/VULNERABILITY_CATALOG.md +++ b/docs/VULNERABILITY_CATALOG.md @@ -7,12 +7,12 @@ from each file's header comment, so this page cannot drift from the source. ## Totals -- **Test cases:** 62 -- **Expected detections:** 62 -- **`VULNERABLE:` markers:** 123 (individual lines a scanner should flag) -- **`SAFE:` markers:** 73 (lines a scanner must not flag — the false-positive control group) +- **Test cases:** 63 +- **Expected detections:** 63 +- **`VULNERABLE:` markers:** 124 (individual lines a scanner should flag) +- **`SAFE:` markers:** 74 (lines a scanner must not flag — the false-positive control group) - **Languages:** 8 — dotenv, go, java, javascript, json, python, ruby, text -- **CWE categories:** 46 — CWE-20, CWE-22, CWE-78, CWE-79, CWE-89, CWE-90, CWE-95, CWE-113, CWE-117, CWE-190, CWE-201, CWE-209, CWE-256, CWE-295, CWE-321, CWE-327, CWE-330, CWE-338, CWE-346, CWE-347, CWE-352, CWE-362, CWE-377, CWE-384, CWE-489, CWE-502, CWE-506, CWE-532, CWE-601, CWE-611, CWE-614, CWE-639, CWE-643, CWE-681, CWE-759, CWE-798, CWE-862, CWE-915, CWE-918, CWE-942, CWE-943, CWE-1236, CWE-1321, CWE-1333, CWE-1336, CWE-1357 +- **CWE categories:** 47 — CWE-20, CWE-22, CWE-78, CWE-79, CWE-89, CWE-90, CWE-95, CWE-113, CWE-117, CWE-190, CWE-201, CWE-209, CWE-256, CWE-295, CWE-300, CWE-321, CWE-327, CWE-330, CWE-338, CWE-346, CWE-347, CWE-352, CWE-362, CWE-377, CWE-384, CWE-489, CWE-502, CWE-506, CWE-532, CWE-601, CWE-611, CWE-614, CWE-639, CWE-643, CWE-681, CWE-759, CWE-798, CWE-862, CWE-915, CWE-918, CWE-942, CWE-943, CWE-1236, CWE-1321, CWE-1333, CWE-1336, CWE-1357 ## How coverage is scored @@ -71,6 +71,7 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | Test case | File | CWE | Severity | Expected | Markers | |---|---|---|---|---|---| | CSV formula injection from untrusted spreadsheet cells | [`csv-formula-injection.py`](../vulns/python/csv-formula-injection.py) | CWE-1236 | medium | yes | 1 vuln / 1 safe | +| DNS rebinding allows SSRF through hostname re-resolution | [`cwe-300-python.py`](../vulns/python/cwe-300-python.py) | CWE-300 | high | yes | 1 vuln / 1 safe | | Excessive user-record exposure in an API response | [`excessive-data-exposure.py`](../vulns/python/excessive-data-exposure.py) | CWE-201 | high | yes | 1 vuln / 1 safe | | Flask debug mode enabled in application configuration | [`flask-debug-enabled.py`](../vulns/python/flask-debug-enabled.py) | CWE-489 | high | yes | 1 vuln / 1 safe | | Hardcoded secret used to configure session signing | [`hardcoded-session-secret.py`](../vulns/python/hardcoded-session-secret.py) | CWE-798 | high | yes | 1 vuln / 1 safe | diff --git a/vulns/VULNERABILITY_CATALOG.json b/vulns/VULNERABILITY_CATALOG.json index 153fdaf..70c4508 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": 62, - "expected_detections": 62, - "vulnerable_markers": 123, - "safe_markers": 73, + "test_cases": 63, + "expected_detections": 63, + "vulnerable_markers": 124, + "safe_markers": 74, "languages": [ "dotenv", "go", @@ -40,6 +40,7 @@ "CWE-209", "CWE-256", "CWE-295", + "CWE-300", "CWE-321", "CWE-327", "CWE-330", @@ -791,6 +792,29 @@ 40 ] }, + { + "id": "py-dns-rebinding-ssrf", + "file": "vulns/python/cwe-300-python.py", + "title": "DNS rebinding allows SSRF through hostname re-resolution", + "category": "python", + "language": "python", + "cwe": "CWE-300", + "cwes": [ + "CWE-300" + ], + "severity": "high", + "expected_detection": true, + "description": "The application resolves a user-supplied hostname twice: once for", + "detection_target": "Taint flow from request input into socket.getaddrinfo or", + "safe_guard": "The entire fixture is wrapped in `if False:` and cannot execute. It", + "attribution": "line", + "vulnerable_lines": [ + 33 + ], + "safe_lines": [ + 44 + ] + }, { "id": "py-excessive-data-exposure", "file": "vulns/python/excessive-data-exposure.py", diff --git a/vulns/python/cwe-300-python.py b/vulns/python/cwe-300-python.py new file mode 100644 index 0000000..4163513 --- /dev/null +++ b/vulns/python/cwe-300-python.py @@ -0,0 +1,45 @@ +""" +@id py-dns-rebinding-ssrf +@test-case DNS rebinding allows SSRF through hostname re-resolution +@cwe CWE-300 +@severity high +@language python +@expected-detection true +@description The application resolves a user-supplied hostname twice: once for + validation and once for the actual request. An attacker can use + DNS rebinding to pass validation with a safe IP, then have the + second resolution point to an internal address, enabling SSRF. +@safe-guard The entire fixture is wrapped in `if False:` and cannot execute. It + uses only example.com and .invalid domains and makes no network + connections. +@detection-target Taint flow from request input into socket.getaddrinfo or + urllib.request.urlopen without pinning the resolved IP + address. + +NEVER RUN IN PRODUCTION - intentional test case for scanner validation. +""" + + +if False: + import socket + import urllib.request + + def fetch_url_vulnerable(request): + hostname = request.args.get("url") # SOURCE: attacker-controlled + # First resolution for validation + ip = socket.gethostbyname(hostname) + if not ip.startswith("10.") and not ip.startswith("192.168."): + # Second resolution happens inside urlopen - vulnerable to rebinding + response = urllib.request.urlopen(f"http://{hostname}/internal") # VULNERABLE: CWE-300 + return response.read() + return "blocked" + + def fetch_url_safe(request): + hostname = request.args.get("url") + # Resolve once and pin the IP + ip = socket.gethostbyname(hostname) + if ip.startswith("10.") or ip.startswith("192.168."): + return "blocked" + # Use the resolved IP directly, not the hostname + response = urllib.request.urlopen(f"http://{ip}/internal", headers={"Host": hostname}) # SAFE: IP is pinned + return response.read() # @expected-detection false \ No newline at end of file