diff --git a/docs/VULNERABILITY_CATALOG.md b/docs/VULNERABILITY_CATALOG.md index 167f585..01acd4c 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:** 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) +- **Test cases:** 127 +- **Expected detections:** 127 +- **`VULNERABLE:` markers:** 236 (individual lines a scanner should flag) +- **`SAFE:` markers:** 144 (lines a scanner must not flag — the false-positive control group) - **Languages:** 8 — dotenv, go, java, javascript, json, python, ruby, text - **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 @@ -92,6 +92,7 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | Server-side request forgery via user-supplied URL | [`ssrf-request-user-url.js`](../vulns/javascript/ssrf-request-user-url.js) | CWE-918 | high | yes | 2 vuln / 2 safe | | TLS certificate validation disabled on an HTTPS agent | [`tls-reject-unauthorized-false.js`](../vulns/javascript/tls-reject-unauthorized-false.js) | CWE-295 | high | yes | 1 vuln / 1 safe | | Unbounded request-body accumulation | [`unbounded-request-body.js`](../vulns/javascript/unbounded-request-body.js) | CWE-400 | high | yes | 1 vuln / 1 safe | +| Password stored using a one-way hash without a salt | [`unsalted-password-hash.js`](../vulns/javascript/unsalted-password-hash.js) | CWE-759 | high | yes | 1 vuln / 1 safe | | Array access with an unvalidated external index | [`unvalidated-array-index.js`](../vulns/javascript/unvalidated-array-index.js) | CWE-129 | medium | yes | 1 vuln / 1 safe | | Login responses reveal whether a username exists | [`username-enumeration-login.js`](../vulns/javascript/username-enumeration-login.js) | CWE-203 | medium | yes | 2 vuln / 1 safe | | Password policy accepts short single-class passwords | [`weak-password-policy.js`](../vulns/javascript/weak-password-policy.js) | CWE-521 | medium | yes | 1 vuln / 1 safe | diff --git a/vulns/VULNERABILITY_CATALOG.json b/vulns/VULNERABILITY_CATALOG.json index dbd217b..8fb4128 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": 126, - "expected_detections": 126, - "vulnerable_markers": 235, - "safe_markers": 143, + "test_cases": 127, + "expected_detections": 127, + "vulnerable_markers": 236, + "safe_markers": 144, "languages": [ "dotenv", "go", @@ -1429,6 +1429,29 @@ 39 ] }, + { + "id": "js-unsalted-password-hash", + "file": "vulns/javascript/unsalted-password-hash.js", + "title": "Password stored using a one-way hash without a salt", + "category": "javascript", + "language": "javascript", + "cwe": "CWE-759", + "cwes": [ + "CWE-759" + ], + "severity": "high", + "expected_detection": true, + "description": "A Node.js signup/login path hashes passwords with a plain", + "detection_target": "Passwords hashed with a one-way digest (sha256/md5) and no", + "safe_guard": "The entire fixture is wrapped in if (false), so no hash is ever", + "attribution": "line", + "vulnerable_lines": [ + 24 + ], + "safe_lines": [ + 34 + ] + }, { "id": "js-unvalidated-array-index", "file": "vulns/javascript/unvalidated-array-index.js", diff --git a/vulns/javascript/unsalted-password-hash.js b/vulns/javascript/unsalted-password-hash.js new file mode 100644 index 0000000..9c49eb3 --- /dev/null +++ b/vulns/javascript/unsalted-password-hash.js @@ -0,0 +1,38 @@ +/** + * @id js-unsalted-password-hash + * @test-case Password stored using a one-way hash without a salt + * @cwe CWE-759 + * @severity high + * @language javascript + * @expected-detection true + * @description A Node.js signup/login path hashes passwords with a plain + * digest and no per-user salt. Identical passwords produce + * identical hashes, enabling rainbow-table and cross-account + * comparison attacks. The safe counterpart uses bcrypt with a + * random per-user salt. + * @safe-guard The entire fixture is wrapped in if (false), so no hash is ever + * computed. No database or request is involved. + * @detection-target Passwords hashed with a one-way digest (sha256/md5) and no + * salt before persistence, instead of a keyed KDF with a + * per-user random salt. + */ + +if (false) { + // NEVER RUN IN PRODUCTION - intentional test case for scanner validation. + function hashPasswordVulnerable(password) { + const crypto = require('crypto'); + // VULNERABLE: CWE-759 unsalted one-way digest of the password + return crypto.createHash('sha256').update(password).digest('hex'); + } + + /** + * Safe counterpart - the scanner should NOT flag this. + * @expected-detection false + */ + async function hashPasswordSafe(password) { + const bcrypt = require('bcrypt'); + // SAFE: bcrypt applies a random per-user salt and many rounds + const salt = await bcrypt.genSalt(12); + return bcrypt.hash(password, salt); + } +} \ No newline at end of file