diff --git a/docs/VULNERABILITY_CATALOG.md b/docs/VULNERABILITY_CATALOG.md index b0c4d61..893738e 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:** 99 -- **Expected detections:** 99 -- **`VULNERABLE:` markers:** 172 (individual lines a scanner should flag) -- **`SAFE:` markers:** 110 (lines a scanner must not flag — the false-positive control group) +- **Test cases:** 100 +- **Expected detections:** 100 +- **`VULNERABLE:` markers:** 175 (individual lines a scanner should flag) +- **`SAFE:` markers:** 111 (lines a scanner must not flag — the false-positive control group) - **Languages:** 8 — dotenv, go, java, javascript, json, python, ruby, text -- **CWE categories:** 77 — CWE-20, CWE-22, CWE-78, CWE-79, CWE-89, CWE-90, 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-327, CWE-329, CWE-330, CWE-338, CWE-345, CWE-346, CWE-347, CWE-352, CWE-362, CWE-377, CWE-384, CWE-400, 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-639, CWE-643, CWE-681, CWE-693, CWE-759, CWE-776, CWE-798, 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 +- **CWE categories:** 78 — 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-327, CWE-329, CWE-330, CWE-338, CWE-345, CWE-346, CWE-347, CWE-352, CWE-362, CWE-377, CWE-384, CWE-400, 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-639, CWE-643, CWE-681, CWE-693, CWE-759, CWE-776, CWE-798, 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 ## How coverage is scored @@ -103,6 +103,7 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. |---|---|---|---|---|---| | Sensitive data sent over an unencrypted channel | [`cleartext-transmission.py`](../vulns/python/cleartext-transmission.py) | CWE-319 | high | yes | 5 vuln / 1 safe | | 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 | +| Code injection via eval() on attacker-controlled input | [`eval-code-injection.py`](../vulns/python/eval-code-injection.py) | CWE-94 | critical | yes | 3 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 f9ade10..9f6186d 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": 99, - "expected_detections": 99, - "vulnerable_markers": 172, - "safe_markers": 110, + "test_cases": 100, + "expected_detections": 100, + "vulnerable_markers": 175, + "safe_markers": 111, "languages": [ "dotenv", "go", @@ -32,6 +32,7 @@ "CWE-79", "CWE-89", "CWE-90", + "CWE-94", "CWE-95", "CWE-113", "CWE-117", @@ -1588,6 +1589,31 @@ 44 ] }, + { + "id": "py-eval-code-injection", + "file": "vulns/python/eval-code-injection.py", + "title": "Code injection via eval() on attacker-controlled input", + "category": "python", + "language": "python", + "cwe": "CWE-94", + "cwes": [ + "CWE-94" + ], + "severity": "critical", + "expected_detection": true, + "description": "An expression built from request-supplied input is passed to", + "detection_target": "eval()/exec() on any data-flow path that touches request", + "safe_guard": "Every payload is wrapped in `if False:` \u2014 unreachable dead code.", + "attribution": "line", + "vulnerable_lines": [ + 28, + 37, + 45 + ], + "safe_lines": [ + 56 + ] + }, { "id": "py-excessive-data-exposure", "file": "vulns/python/excessive-data-exposure.py", diff --git a/vulns/python/eval-code-injection.py b/vulns/python/eval-code-injection.py new file mode 100644 index 0000000..1446d4b --- /dev/null +++ b/vulns/python/eval-code-injection.py @@ -0,0 +1,63 @@ +""" +@id py-eval-code-injection +@test-case Code injection via eval() on attacker-controlled input +@cwe CWE-94 +@severity critical +@language python +@expected-detection true +@description An expression built from request-supplied input is passed to + eval(), which executes arbitrary Python in the server process. + A second variant routes the payload through exec() on a + de-obfuscated byte-string, the same sink reached from an + attacker-controlled payload. +@safe-guard Every payload is wrapped in `if False:` — unreachable dead code. + There is no import of any live module and no socket/disk write. +@detection-target eval()/exec() on any data-flow path that touches request + input, even through str.replace or base64 decoding. + +NEVER RUN IN PRODUCTION — intentional test case for scanner validation. +""" + +import base64 + + +def eval_math_vulnerable(request): + if False: + # SOURCE: attacker-controlled expression string + expr = request.args["expr"] + # VULNERABLE: CWE-94 — eval executes arbitrary Python + return eval(expr) + return None + + +def eval_obfuscated_vulnerable(request): + if False: + # SOURCE: attacker-controlled base64 payload, decoded then executed + payload = base64.b64decode(request.args["b64"]).decode() + # VULNERABLE: CWE-94 — decoding does not make the input safe + return eval(payload) + return None + + +def exec_vulnerable(request): + if False: + code = request.form.get("code") + # VULNERABLE: CWE-94 — exec runs a full statement suite + exec(code) + return None + + +def eval_whitelist_safe(request): + """Safe counterpart — the scanner should NOT flag this. + + @expected-detection false + """ + if False: + # SAFE: only a fixed set of numeric operators is ever evaluated, + # and ast.literal_eval never runs arbitrary code. + import ast + tokens = set(request.args["expr"].split()) + if not tokens.issubset({"0", "1", "+", "-", "*", "/", "(", ")"}): + return None + return ast.literal_eval(request.args["expr"]) + return None