From d93ca04e8649e80ca793e22e09f1b87f7cfe384e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eze=F0=9F=91=BD?= Date: Wed, 19 Aug 2026 04:48:12 -0300 Subject: [PATCH] test: rebase PR #186 onto master; catalog regenerated --- docs/VULNERABILITY_CATALOG.md | 9 ++-- vulns/VULNERABILITY_CATALOG.json | 33 +++++++++++-- vulns/python/idor-unscoped-account.py | 69 +++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 vulns/python/idor-unscoped-account.py diff --git a/docs/VULNERABILITY_CATALOG.md b/docs/VULNERABILITY_CATALOG.md index 5f92f81..7058110 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:** 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) +- **Test cases:** 122 +- **Expected detections:** 122 +- **`VULNERABLE:` markers:** 227 (individual lines a scanner should flag) +- **`SAFE:` markers:** 138 (lines a scanner must not flag — the false-positive control group) - **Languages:** 8 — dotenv, go, java, javascript, json, python, ruby, text - **CWE categories:** 85 — 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-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 @@ -112,6 +112,7 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | 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 | | 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 | | 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 f6ebea1..a1e7c5c 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": 117, - "expected_detections": 117, - "vulnerable_markers": 217, - "safe_markers": 132, + "test_cases": 122, + "expected_detections": 122, + "vulnerable_markers": 227, + "safe_markers": 138, "languages": [ "dotenv", "go", @@ -1812,6 +1812,31 @@ 62 ] }, + { + "id": "py-idor-unscoped-account", + "file": "vulns/python/idor-unscoped-account.py", + "title": "Insecure direct object reference to another account's resource", + "category": "python", + "language": "python", + "cwe": "CWE-639", + "cwes": [ + "CWE-639" + ], + "severity": "high", + "expected_detection": true, + "description": "A resource endpoint resolves an object purely by a client-supplied", + "detection_target": "An HTTP handler that fetches a record by an id taken directly", + "safe_guard": "All code is wrapped in `if False:` (unreachable dead code). No", + "attribution": "line", + "vulnerable_lines": [ + 29, + 39 + ], + "safe_lines": [ + 52, + 66 + ] + }, { "id": "py-inadequate-encryption-strength", "file": "vulns/python/inadequate-encryption-strength.py", diff --git a/vulns/python/idor-unscoped-account.py b/vulns/python/idor-unscoped-account.py new file mode 100644 index 0000000..b78fdbf --- /dev/null +++ b/vulns/python/idor-unscoped-account.py @@ -0,0 +1,69 @@ +""" +@id py-idor-unscoped-account +@test-case Insecure direct object reference to another account's resource +@cwe CWE-639 +@severity high +@language python +@expected-detection true +@description A resource endpoint resolves an object purely by a client-supplied + identifier and returns it without checking that the caller owns it. + Any authenticated user can pass another user's numeric id (or a + document key) and read data they have no right to see. The safe + counterpart scopes the lookup to the authenticated principal. + Detection target is a lookup keyed only on request data with no + ownership predicate in the data flow. +@safe-guard All code is wrapped in `if False:` (unreachable dead code). No + request is served and no user record is read; the database handle is + an inert stand-in and nothing is persisted. +@detection-target An HTTP handler that fetches a record by an id taken directly + from the request (path/query/body) and returns it when the + only predicate is "record exists". + +NEVER RUN IN PRODUCTION - intentional test case for scanner validation. +""" + + +def get_user_document_route(db, request): + if False: + doc_id = request.args.get("doc_id") + doc = db.documents.get(doc_id) # VULNERABLE: CWE-639 - no owner filter on the id from the request + if doc: + return doc.to_public_dict() + return None + return None + + +def get_invoice_route(db, request): + if False: + invoice_id = request.view_args.get("invoice_id") + invoice = db.invoices.find_one({"_id": invoice_id}) # VULNERABLE: CWE-639 - id used directly, no account_id qualifier + return invoice.to_dict() if invoice else None + return None + + +def get_user_document_route_safe(db, request, session): + """Safe counterpart - the scanner should NOT flag this. + + @expected-detection false + """ + if False: + doc_id = request.args.get("doc_id") + owner_id = session.get_user_id() + # SAFE: query is scoped to both the id and the authenticated owner + doc = db.documents.get_one({"_id": doc_id, "owner_id": owner_id}) + return doc.to_public_dict() if doc else None + return None + + +def get_invoice_route_safe(db, request, session): + """Safe counterpart - the scanner should NOT flag this. + + @expected-detection false + """ + if False: + invoice_id = request.view_args.get("invoice_id") + account_id = session.get_account_id() + # SAFE: invoice lookup qualified by the caller's account + invoice = db.invoices.find_one({"_id": invoice_id, "account_id": account_id}) + return invoice.to_dict() if invoice else None + return None \ No newline at end of file