This is one of the more surprising bugs, because the vulnerable code looks completely right: you compare the token the caller sent against the real one with ===. The problem is not correctness; it is that a normal comparison stops at the first differing character, and that tiny difference in timing is a signal an attacker can measure and exploit.
How a timing attack works
A standard equality check returns as soon as it finds a character that does not match. So a guess that is wrong on the first character returns very slightly faster than one that is wrong on the tenth. By making many attempts and measuring the response time, an attacker learns how many leading characters they have right, then extends the correct prefix one character at a time until they reconstruct the whole secret.
// the vulnerable pattern: normal comparison on a secret
if (req.headers["x-api-key"] === process.env.API_KEY) {
// returns early on the first mismatched character -> timing leak
}Want to see exactly what Prbl flags? Watch it scan a demo app, no repo or account needed.
See a live scan →The fix: compare in constant time
Use a timing-safe comparison that always takes the same time regardless of where the values differ, so there is no signal to measure:
import crypto from "node:crypto";
function safeEqual(a, b) {
const ab = Buffer.from(a);
const bb = Buffer.from(b);
return ab.length === bb.length && crypto.timingSafeEqual(ab, bb);
}
if (safeEqual(req.headers["x-api-key"] ?? "", process.env.API_KEY)) {
// constant-time: no timing information leaks
}Python has hmac.compare_digest for the same purpose. Use it anywhere you compare a raw secret an attacker can submit: API keys, webhook signatures, reset and verification tokens. The full walkthrough is in the fix for a timing-unsafe token comparison. For passwords, a slow hash like bcrypt already compares safely, as covered in weak password hashing.
Frequently asked questions
How can comparing two strings leak a secret?
A normal equality check stops at the first character that differs, so a comparison that fails on the second character returns slightly faster than one that fails on the tenth. By measuring those tiny timing differences across many attempts, an attacker can learn how many leading characters they got right, and extend a guess one character at a time until they have the whole token. It is slow, but for a high-value secret it is practical.
Which comparisons are at risk?
Any place you compare a secret an attacker can submit against a stored value: an API key check, a webhook signature comparison, a password-reset or verification token, a session token. If the comparison is a normal == or === on the secret, and the attacker can make many attempts and measure timing, it is a candidate. Public, non-secret comparisons do not matter.
What is a timing-safe comparison?
It is a comparison that always takes the same amount of time regardless of where the two values differ, so no timing information leaks. Languages provide a built-in for this: crypto.timingSafeEqual in Node, hmac.compare_digest in Python. They compare the full length every time instead of stopping early, which removes the signal the attack relies on.
Do I need this for password checks too?
For passwords you should already be using a slow hash like bcrypt, and its compare function is timing-safe, so a proper password check is covered. The timing-safe comparison matters most for raw secret tokens you compare directly, like API keys and signatures, where you are not running them through a password hash.
Check your secret comparisons
A normal equality check on an API key or signature is easy to miss because it works perfectly; the flaw is only in the timing. A scan flags a secret compared without a timing-safe function. Run a free scan and see how your app compares its secrets.