From e7b4beaeabaa348262dd1ec83251471b249489fe Mon Sep 17 00:00:00 2001 From: RissRIce Date: Sat, 8 Aug 2026 01:50:53 -0600 Subject: [PATCH] Add timing-unsafe HMAC comparison test case --- docs/VULNERABILITY_CATALOG.md | 11 +++--- vulns/VULNERABILITY_CATALOG.json | 32 +++++++++++++-- .../javascript/hmac-timing-unsafe-compare.js | 39 +++++++++++++++++++ 3 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 vulns/javascript/hmac-timing-unsafe-compare.js diff --git a/docs/VULNERABILITY_CATALOG.md b/docs/VULNERABILITY_CATALOG.md index 5763d34..d104426 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:** 62 -- **Expected detections:** 62 -- **`VULNERABLE:` markers:** 123 (individual lines a scanner should flag) -- **`SAFE:` markers:** 73 (lines a scanner must not flag — the false-positive control group) +- **Test cases:** 63 +- **Expected detections:** 63 +- **`VULNERABLE:` markers:** 124 (individual lines a scanner should flag) +- **`SAFE:` markers:** 74 (lines a scanner must not flag — the false-positive control group) - **Languages:** 8 — dotenv, go, java, javascript, json, python, ruby, text -- **CWE categories:** 46 — CWE-20, CWE-22, CWE-78, CWE-79, CWE-89, CWE-90, CWE-95, CWE-113, CWE-117, CWE-190, CWE-201, CWE-209, CWE-256, CWE-295, CWE-321, CWE-327, CWE-330, CWE-338, CWE-346, CWE-347, CWE-352, CWE-362, CWE-377, CWE-384, CWE-489, CWE-502, CWE-506, CWE-532, CWE-601, CWE-611, CWE-614, CWE-639, CWE-643, CWE-681, CWE-759, CWE-798, CWE-862, CWE-915, CWE-918, CWE-942, CWE-943, CWE-1236, CWE-1321, CWE-1333, CWE-1336, CWE-1357 +- **CWE categories:** 47 — CWE-20, CWE-22, CWE-78, CWE-79, CWE-89, CWE-90, CWE-95, CWE-113, CWE-117, CWE-190, CWE-201, CWE-208, CWE-209, CWE-256, CWE-295, CWE-321, CWE-327, CWE-330, CWE-338, CWE-346, CWE-347, CWE-352, CWE-362, CWE-377, CWE-384, CWE-489, CWE-502, CWE-506, CWE-532, CWE-601, CWE-611, CWE-614, CWE-639, CWE-643, CWE-681, CWE-759, CWE-798, CWE-862, CWE-915, CWE-918, CWE-942, CWE-943, CWE-1236, CWE-1321, CWE-1333, CWE-1336, CWE-1357 ## How coverage is scored @@ -48,6 +48,7 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | Credentialed CORS configured with a wildcard origin | [`cors-wildcard-credentials.js`](../vulns/javascript/cors-wildcard-credentials.js) | CWE-942 | high | yes | 1 vuln / 1 safe | | 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 | | Hardcoded cryptographic key used for application encryption | [`hardcoded-crypto-key.js`](../vulns/javascript/hardcoded-crypto-key.js) | CWE-321 | high | yes | 1 vuln / 1 safe | +| Timing-unsafe comparison of an HMAC webhook signature | [`hmac-timing-unsafe-compare.js`](../vulns/javascript/hmac-timing-unsafe-compare.js) | CWE-208 | medium | yes | 1 vuln / 1 safe | | Password-reset URL poisoning via untrusted Host header | [`host-header-password-reset.js`](../vulns/javascript/host-header-password-reset.js) | CWE-346 | high | yes | 1 vuln / 1 safe | | HTTP response splitting via unsanitized header value | [`http-header-injection.js`](../vulns/javascript/http-header-injection.js) | CWE-113 | high | yes | 1 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 | diff --git a/vulns/VULNERABILITY_CATALOG.json b/vulns/VULNERABILITY_CATALOG.json index 153fdaf..264bd48 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": 62, - "expected_detections": 62, - "vulnerable_markers": 123, - "safe_markers": 73, + "test_cases": 63, + "expected_detections": 63, + "vulnerable_markers": 124, + "safe_markers": 74, "languages": [ "dotenv", "go", @@ -37,6 +37,7 @@ "CWE-117", "CWE-190", "CWE-201", + "CWE-208", "CWE-209", "CWE-256", "CWE-295", @@ -367,6 +368,29 @@ 30 ] }, + { + "id": "js-hmac-timing-unsafe-compare", + "file": "vulns/javascript/hmac-timing-unsafe-compare.js", + "title": "Timing-unsafe comparison of an HMAC webhook signature", + "category": "javascript", + "language": "javascript", + "cwe": "CWE-208", + "cwes": [ + "CWE-208" + ], + "severity": "medium", + "expected_detection": true, + "description": "A computed HMAC is compared to an attacker-supplied signature", + "detection_target": "A secret-derived HMAC digest compared to untrusted input", + "safe_guard": "Both examples are behind if (false), receive only caller-supplied", + "attribution": "line", + "vulnerable_lines": [ + 24 + ], + "safe_lines": [ + 35 + ] + }, { "id": "js-host-header-password-reset", "file": "vulns/javascript/host-header-password-reset.js", diff --git a/vulns/javascript/hmac-timing-unsafe-compare.js b/vulns/javascript/hmac-timing-unsafe-compare.js new file mode 100644 index 0000000..1b6c05e --- /dev/null +++ b/vulns/javascript/hmac-timing-unsafe-compare.js @@ -0,0 +1,39 @@ +/** + * @id js-hmac-timing-unsafe-compare + * @test-case Timing-unsafe comparison of an HMAC webhook signature + * @cwe CWE-208 + * @severity medium + * @language javascript + * @expected-detection true + * @description A computed HMAC is compared to an attacker-supplied signature + * with ordinary string equality, which can return after the first + * mismatch and expose prefix information through response timing. + * @safe-guard Both examples are behind if (false), receive only caller-supplied + * values, perform no I/O, and cannot execute or contact a service. + * @detection-target A secret-derived HMAC digest compared to untrusted input + * with === instead of a constant-time comparison primitive. + */ + +'use strict'; + +const crypto = require('crypto'); + +function verifyWebhookVulnerable(body, suppliedSignature, secret) { + if (false) { + const expectedSignature = crypto.createHmac('sha256', secret).update(body).digest('hex'); + return expectedSignature === suppliedSignature; // VULNERABLE: CWE-208 timing-unsafe MAC comparison + } +} + +/** Safe counterpart. @expected-detection false */ +function verifyWebhookSafe(body, suppliedSignature, secret) { + if (false) { + const expectedSignature = crypto.createHmac('sha256', secret).update(body).digest(); + const candidateSignature = Buffer.from(suppliedSignature, 'hex'); + + return candidateSignature.length === expectedSignature.length + && crypto.timingSafeEqual(expectedSignature, candidateSignature); // SAFE: equal-length buffers compared in constant time + } +} + +module.exports = { verifyWebhookVulnerable, verifyWebhookSafe };