From 23c448b21c191799eefa7e520be394ef44b140ba Mon Sep 17 00:00:00 2001 From: RissRIce Date: Tue, 4 Aug 2026 01:21:08 -0600 Subject: [PATCH] Add unscoped object lookup IDOR test case --- docs/VULNERABILITY_CATALOG.md | 11 ++-- vulns/VULNERABILITY_CATALOG.json | 32 +++++++++-- .../javascript/idor-unscoped-object-lookup.js | 55 +++++++++++++++++++ 3 files changed, 89 insertions(+), 9 deletions(-) create mode 100644 vulns/javascript/idor-unscoped-object-lookup.js diff --git a/docs/VULNERABILITY_CATALOG.md b/docs/VULNERABILITY_CATALOG.md index dc60625..98aa751 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:** 38 -- **Expected detections:** 38 -- **`VULNERABLE:` markers:** 92 (individual lines a scanner should flag) -- **`SAFE:` markers:** 46 (lines a scanner must not flag — the false-positive control group) +- **Test cases:** 39 +- **Expected detections:** 39 +- **`VULNERABLE:` markers:** 93 (individual lines a scanner should flag) +- **`SAFE:` markers:** 47 (lines a scanner must not flag — the false-positive control group) - **Languages:** 8 — dotenv, go, java, javascript, json, python, ruby, text -- **CWE categories:** 23 — CWE-22, CWE-78, CWE-79, CWE-89, CWE-95, CWE-190, CWE-209, CWE-347, CWE-352, CWE-362, CWE-377, CWE-502, CWE-506, CWE-532, CWE-601, CWE-611, CWE-681, CWE-798, CWE-918, CWE-1321, CWE-1333, CWE-1336, CWE-1357 +- **CWE categories:** 24 — CWE-22, CWE-78, CWE-79, CWE-89, CWE-95, CWE-190, CWE-209, CWE-347, CWE-352, CWE-362, CWE-377, CWE-502, CWE-506, CWE-532, CWE-601, CWE-611, CWE-639, CWE-681, CWE-798, CWE-918, CWE-1321, CWE-1333, CWE-1336, CWE-1357 ## How coverage is scored @@ -45,6 +45,7 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | Test case | File | CWE | Severity | Expected | Markers | |---|---|---|---|---|---| | CSRF via missing anti-CSRF token on state-changing POST | [`csrf-missing-token.js`](../vulns/javascript/csrf-missing-token.js) | CWE-352 | high | yes | 3 vuln / 1 safe | +| IDOR via unscoped object lookup | [`idor-unscoped-object-lookup.js`](../vulns/javascript/idor-unscoped-object-lookup.js) | CWE-639 | high | yes | 1 vuln / 1 safe | | JWT signature validation bypass via decode-only parsing | [`jwt-decode-without-verify.js`](../vulns/javascript/jwt-decode-without-verify.js) | CWE-347 | high | yes | 1 vuln / 1 safe | | Open redirect via unvalidated next parameter | [`open-redirect.js`](../vulns/javascript/open-redirect.js) | CWE-601 | medium | yes | 3 vuln / 1 safe | | Prototype pollution via recursive merge | [`prototype-pollution.js`](../vulns/javascript/prototype-pollution.js) | CWE-1321 | high | yes | 2 vuln / 1 safe | diff --git a/vulns/VULNERABILITY_CATALOG.json b/vulns/VULNERABILITY_CATALOG.json index 73ce62d..1557680 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": 38, - "expected_detections": 38, - "vulnerable_markers": 92, - "safe_markers": 46, + "test_cases": 39, + "expected_detections": 39, + "vulnerable_markers": 93, + "safe_markers": 47, "languages": [ "dotenv", "go", @@ -42,6 +42,7 @@ "CWE-532", "CWE-601", "CWE-611", + "CWE-639", "CWE-681", "CWE-798", "CWE-918", @@ -275,6 +276,29 @@ 55 ] }, + { + "id": "js-idor-unscoped-object-lookup", + "file": "vulns/javascript/idor-unscoped-object-lookup.js", + "title": "IDOR via unscoped object lookup", + "category": "javascript", + "language": "javascript", + "cwe": "CWE-639", + "cwes": [ + "CWE-639" + ], + "severity": "high", + "expected_detection": true, + "description": "An authenticated Express handler fetches an invoice using only", + "detection_target": "Object lookup keyed only by a request parameter despite an", + "safe_guard": "Wrapped in if (false) \u2014 every lookup and response is unreachable", + "attribution": "line", + "vulnerable_lines": [ + 24 + ], + "safe_lines": [ + 37 + ] + }, { "id": "js-jwt-decode-without-verify", "file": "vulns/javascript/jwt-decode-without-verify.js", diff --git a/vulns/javascript/idor-unscoped-object-lookup.js b/vulns/javascript/idor-unscoped-object-lookup.js new file mode 100644 index 0000000..6bb0423 --- /dev/null +++ b/vulns/javascript/idor-unscoped-object-lookup.js @@ -0,0 +1,55 @@ +/** + * @id js-idor-unscoped-object-lookup + * @test-case IDOR via unscoped object lookup + * @cwe CWE-639 + * @severity high + * @language javascript + * @expected-detection true + * @description An authenticated Express handler fetches an invoice using only + * an attacker-controlled route identifier. Because the lookup is + * not constrained to the current user, changing the identifier + * can expose another user's object. + * @safe-guard Wrapped in if (false) — every lookup and response is unreachable + * dead code. The database helper is a local inert placeholder. + * @detection-target Object lookup keyed only by a request parameter despite an + * authenticated owner identifier being available. + */ + +'use strict'; + +// NEVER RUN IN PRODUCTION — intentional test case for scanner validation. +function getInvoiceVulnerable(req, res) { + if (false) { + const invoiceId = req.params.invoiceId; // SOURCE: attacker-controlled ID + const invoice = database.findInvoiceById(invoiceId); // VULNERABLE: CWE-639 — lookup is not scoped to req.session.userId + return res.json(invoice); + } +} + +/** + * Safe counterpart — the scanner should NOT flag this. + * @expected-detection false + */ +function getInvoiceSafe(req, res) { + if (false) { + const invoiceId = req.params.invoiceId; + const ownerId = req.session.userId; + const invoice = database.findInvoiceByIdAndOwner(invoiceId, ownerId); // SAFE: ownership is enforced in the lookup + if (!invoice) { + return res.status(404).send('not found'); + } + return res.json(invoice); + } +} + +// Local inert placeholder so the file parses without a database dependency. +const database = { + findInvoiceById(invoiceId) { + return { id: invoiceId }; + }, + findInvoiceByIdAndOwner(invoiceId, ownerId) { + return ownerId ? { id: invoiceId, ownerId } : null; + }, +}; + +module.exports = { getInvoiceVulnerable, getInvoiceSafe };