diff --git a/docs/VULNERABILITY_CATALOG.md b/docs/VULNERABILITY_CATALOG.md index 69a6a0c..d9816ec 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:** 31 -- **Expected detections:** 31 -- **`VULNERABLE:` markers:** 76 (individual lines a scanner should flag) -- **`SAFE:` markers:** 39 (lines a scanner must not flag — the false-positive control group) +- **Test cases:** 32 +- **Expected detections:** 32 +- **`VULNERABLE:` markers:** 77 (individual lines a scanner should flag) +- **`SAFE:` markers:** 40 (lines a scanner must not flag — the false-positive control group) - **Languages:** 8 — dotenv, go, java, javascript, json, python, ruby, text -- **CWE categories:** 18 — CWE-22, CWE-78, CWE-79, CWE-89, CWE-95, CWE-190, CWE-362, CWE-377, CWE-502, CWE-506, CWE-532, CWE-601, CWE-681, CWE-798, CWE-918, CWE-1321, CWE-1336, CWE-1357 +- **CWE categories:** 19 — CWE-22, CWE-78, CWE-79, CWE-89, CWE-95, CWE-190, CWE-347, CWE-362, CWE-377, CWE-502, CWE-506, CWE-532, CWE-601, CWE-681, CWE-798, CWE-918, CWE-1321, CWE-1336, CWE-1357 ## How coverage is scored @@ -43,6 +43,7 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | Test case | File | CWE | Severity | Expected | Markers | |---|---|---|---|---|---| +| 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 | | OS command injection via child_process.exec | [`rce-child-process.js`](../vulns/javascript/rce-child-process.js) | CWE-78 | critical | yes | 2 vuln / 1 safe | diff --git a/vulns/VULNERABILITY_CATALOG.json b/vulns/VULNERABILITY_CATALOG.json index 1eaaefd..77940d7 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": 31, - "expected_detections": 31, - "vulnerable_markers": 76, - "safe_markers": 39, + "test_cases": 32, + "expected_detections": 32, + "vulnerable_markers": 77, + "safe_markers": 40, "languages": [ "dotenv", "go", @@ -32,6 +32,7 @@ "CWE-89", "CWE-95", "CWE-190", + "CWE-347", "CWE-362", "CWE-377", "CWE-502", @@ -221,6 +222,29 @@ 56 ] }, + { + "id": "js-jwt-decode-without-verify", + "file": "vulns/javascript/jwt-decode-without-verify.js", + "title": "JWT signature validation bypass via decode-only parsing", + "category": "javascript", + "language": "javascript", + "cwe": "CWE-347", + "cwes": [ + "CWE-347" + ], + "severity": "high", + "expected_detection": true, + "description": "Authorization trusts role claims returned by jwt.decode(), which", + "detection_target": "Security-sensitive claims from jwt.decode() used for an", + "safe_guard": "Both examples are wrapped in if (false), so neither token path can", + "attribution": "line", + "vulnerable_lines": [ + 25 + ], + "safe_lines": [ + 44 + ] + }, { "id": "js-open-redirect", "file": "vulns/javascript/open-redirect.js", diff --git a/vulns/javascript/jwt-decode-without-verify.js b/vulns/javascript/jwt-decode-without-verify.js new file mode 100644 index 0000000..7136910 --- /dev/null +++ b/vulns/javascript/jwt-decode-without-verify.js @@ -0,0 +1,52 @@ +/** + * @id js-jwt-decode-without-verify + * @test-case JWT signature validation bypass via decode-only parsing + * @cwe CWE-347 + * @severity high + * @language javascript + * @expected-detection true + * @description Authorization trusts role claims returned by jwt.decode(), which + * parses attacker-controlled token contents without validating the + * signature, issuer, audience, or signing algorithm. + * @safe-guard Both examples are wrapped in if (false), so neither token path can + * execute. The safe example names only reserved .invalid hosts. + * @detection-target Security-sensitive claims from jwt.decode() used for an + * authorization decision without a preceding jwt.verify(). + */ + +'use strict'; + +const jwt = require('jsonwebtoken'); + +// NEVER RUN IN PRODUCTION — intentional test case for scanner validation. +function authorizeDecodedToken(req, res) { + if (false) { + const token = req.headers.authorization.replace(/^Bearer\s+/i, ''); + const claims = jwt.decode(token); // VULNERABLE: CWE-347, signature is never verified + + if (claims && claims.role === 'admin') { + res.json({ authorized: true }); + } + } +} + +/** + * Safe counterpart — the scanner should NOT flag this. + * @expected-detection false + */ +function authorizeVerifiedToken(req, res) { + if (false) { + const token = req.headers.authorization.replace(/^Bearer\s+/i, ''); + const claims = jwt.verify(token, process.env.JWT_PUBLIC_KEY, { + algorithms: ['RS256'], + audience: 'scanner.example.invalid', + issuer: 'https://issuer.example.invalid', + }); // SAFE: signature, algorithm, issuer, and audience are verified + + if (claims.role === 'admin') { + res.json({ authorized: true }); + } + } +} + +module.exports = { authorizeDecodedToken, authorizeVerifiedToken };