So sánh chuỗi thông thường dừng ngay tại byte đầu tiên khác nhau.
Thời gian chạy vì thế phụ thuộc vào số ký tự đầu khớp đúng — kẻ tấn công đo thời gian phản hồi qua nhiều lần thử có thể dựng lại token/chữ ký từng byte một, biến việc bẻ khoá từ vét cạn toàn không gian thành tuyến tính theo độ dài.
// Wrong: bails out at the first differing byte
if (providedSignature === expectedSignature) { /* ... */ }
// Right: constant time, both operands must have equal length
import { timingSafeEqual } from 'node:crypto'
const a = Buffer.from(providedSignature)
const b = Buffer.from(expectedSignature)
const ok = a.length === b.length && timingSafeEqual(a, b)Hàm tương đương ở các nền tảng khác: hmac.compare_digest (Python), hash_equals (PHP), subtle.ConstantTimeCompare (Go), MessageDigest.isEqual (Java).
Áp dụng cho những giá trị nào: chữ ký webhook (SePay, Stripe, GitHub), API key, token reset mật khẩu, session id khi tự so sánh, mã OTP/TOTP, HMAC nói chung — nghĩa là mọi bí mật mà client gửi lên để đối chiếu.
Không cần cho: kết quả bcrypt.compare / argon2.verify (thư viện đã lo), và mật khẩu dạng rõ (không bao giờ so sánh trực tiếp).
Mẹo tránh phụ thuộc vào độ dài: timingSafeEqual ném lỗi khi hai buffer khác độ dài, và bản thân việc lộ độ dài cũng là rò rỉ nhỏ. Cách gọn là hash cả hai giá trị bằng SHA-256 rồi so sánh hai digest — luôn cùng 32 byte, so sánh hằng thời gian, không lộ gì về độ dài đầu vào.
Trên môi trường thực (mạng có nhiễu, JIT, GC) khai thác timing khó hơn lý thuyết, nhưng chi phí phòng thủ gần bằng không nên không có lý do bỏ qua.