diff --git a/docs/VULNERABILITY_CATALOG.md b/docs/VULNERABILITY_CATALOG.md index 0ea56a5..167f585 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:** 125 -- **Expected detections:** 125 -- **`VULNERABLE:` markers:** 233 (individual lines a scanner should flag) -- **`SAFE:` markers:** 142 (lines a scanner must not flag — the false-positive control group) +- **Test cases:** 126 +- **Expected detections:** 126 +- **`VULNERABLE:` markers:** 235 (individual lines a scanner should flag) +- **`SAFE:` markers:** 143 (lines a scanner must not flag — the false-positive control group) - **Languages:** 8 — dotenv, go, java, javascript, json, python, ruby, text -- **CWE categories:** 88 — 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-311, CWE-312, 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-409, 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-522, 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 +- **CWE categories:** 89 — 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-285, CWE-287, CWE-288, CWE-291, CWE-295, CWE-306, CWE-307, CWE-311, CWE-312, 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-409, 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-522, 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 ## How coverage is scored @@ -114,6 +114,7 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | 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 | | HTTP response header injection via user-controlled header value | [`http-header-injection.py`](../vulns/python/http-header-injection.py) | CWE-113 | high | yes | 3 vuln / 1 safe | | Insecure direct object reference to another account's resource | [`idor-unscoped-account.py`](../vulns/python/idor-unscoped-account.py) | CWE-639 | high | yes | 2 vuln / 2 safe | +| Improper authorization via role bypass using role identity only | [`improper-authorization-role-id.py`](../vulns/python/improper-authorization-role-id.py) | CWE-285 | high | yes | 2 vuln / 1 safe | | Inadequate encryption strength at the credential boundary | [`inadequate-encryption-strength.py`](../vulns/python/inadequate-encryption-strength.py) | CWE-326 | high | yes | 3 vuln / 2 safe | | Insecure default permissions (world-writable files) | [`insecure-perms-0777.py`](../vulns/python/insecure-perms-0777.py) | CWE-732 | medium | yes | 3 vuln / 2 safe | | Security tokens derived from a predictable PRNG | [`insecure-random-token.py`](../vulns/python/insecure-random-token.py) | CWE-338 | high | yes | 4 vuln / 3 safe | diff --git a/vulns/VULNERABILITY_CATALOG.json b/vulns/VULNERABILITY_CATALOG.json index ce63e76..dbd217b 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": 125, - "expected_detections": 125, - "vulnerable_markers": 233, - "safe_markers": 142, + "test_cases": 126, + "expected_detections": 126, + "vulnerable_markers": 235, + "safe_markers": 143, "languages": [ "dotenv", "go", @@ -43,6 +43,7 @@ "CWE-208", "CWE-209", "CWE-256", + "CWE-285", "CWE-287", "CWE-288", "CWE-291", @@ -1864,6 +1865,30 @@ 66 ] }, + { + "id": "py-improper-authorization-role-id", + "file": "vulns/python/improper-authorization-role-id.py", + "title": "Improper authorization via role bypass using role identity only", + "category": "python", + "language": "python", + "cwe": "CWE-285", + "cwes": [ + "CWE-285" + ], + "severity": "high", + "expected_detection": true, + "description": "A Flask/FastAPI endpoint gates an admin action by checking that", + "detection_target": "Authorization decision based solely on a client-supplied or", + "safe_guard": "Wrapped in `if False:` \u2014 unreachable dead code, no route or request", + "attribution": "line", + "vulnerable_lines": [ + 28, + 38 + ], + "safe_lines": [ + 53 + ] + }, { "id": "py-inadequate-encryption-strength", "file": "vulns/python/inadequate-encryption-strength.py", diff --git a/vulns/python/improper-authorization-role-id.py b/vulns/python/improper-authorization-role-id.py new file mode 100644 index 0000000..5212317 --- /dev/null +++ b/vulns/python/improper-authorization-role-id.py @@ -0,0 +1,75 @@ +""" +@id py-improper-authorization-role-id +@test-case Improper authorization via role bypass using role identity only +@cwe CWE-285 +@severity high +@language python +@expected-detection true +@description A Flask/FastAPI endpoint gates an admin action by checking that + the role id is present in the session, but never re-validates the + role against the authenticated user's actual membership. An + attacker who can forge or reuse a role claim gains admin + privileges without a corresponding authorization check. The safe + counterpart validates the role against an external membership + source on every request. +@safe-guard Wrapped in `if False:` — unreachable dead code, no route or request + is ever served. +@detection-target Authorization decision based solely on a client-supplied or + cached role claim without re-validating membership against + the canonical source. + +NEVER RUN IN PRODUCTION — intentional test case for scanner validation. +""" + + +def admin_dashboard_vulnerable(role_id): + if False: + from flask import request + # VULNERABLE: CWE-285 — role claim trusted without membership re-check + if request.session.get("role_id") == "admin": + return render_admin_panel() + return "forbidden" + return None + + +def transfer_authorized_vulnerable(role_id, account): + if False: + from flask import request + # VULNERABLE: CWE-285 — role id accepted as proof of privilege + role = request.headers.get("X-Role") + if role == "operator": + execute_transfer(account, amount=request.form.get("amount")) + return "done" + return None + + +def admin_dashboard_safe(role_id): + """Safe counterpart — the scanner should NOT flag this. + + @expected-detection false + """ + if False: + from flask import request + # SAFE: membership re-validated against canonical source each request + member = membership_lookup(request.user.id) + if member.role == "admin" and member.active: + return render_admin_panel() + return "forbidden" + return None + + +# Placeholders so the file parses standalone; never invoked. +def render_admin_panel(): + return "panel" + + +def execute_transfer(account, amount): + return account and amount + + +def membership_lookup(user_id): + class M: + role = "user" + active = True + + return M() \ No newline at end of file