Let’s start from the very beginning:
What a timing attack is (CWE‑208), why it matters in Node.js, and exactly how you can stop it.
CWE stands for
Common Weakness Enumeration
, a catalog of software security flaws.CWE‑208 covers flaws where an attacker learns secret data (passwords, keys, etc.) simply by measuring how long certain operations take.
if your code takes a slightly different amount of time depending on secret data, an attacker can “listen to the clock” and gradually recover that secret.
What’s a timing (side‑channel) attack?
The side‑channel idea
Imagine you’re behind a curtain watching two people entering a dark room and opening a safe with a code lock. You can’t see the lock, but you hear the clicks.
If one person pauses longer before entering the final digit, you realize they got three digits right and hesitated on the fourth. By measuring those pauses, you learn the code—without ever seeing it.
In computing, a side‑channel is any unintended “signal” (time, power use, sound) that leaks information about what’s happening inside.
A timing attack is a side‑channel attack that focuses on tiny time differences.
Why timing matters
Most programming languages stop a compare operation (“do these two values match?”) as soon as they find a mismatch.
- Compare
abcX
vsabcY
: mismatch on the 4th character → stop after 4 checks. - Compare
a000
vsb000
: mismatch on the 1st character → stop after 1 check.
An attacker can repeatedly try different guesses and carefully measure “how long did the server take to reject it?”
Over thousands of requests, those micro‑differences reveal which characters were matching.
How do timing leaks show up in Node.js?
Node.js, by default, uses C libraries or pure‑JavaScript routines that often bail out early on a mismatch. Common pitfalls:
1.String or buffer comparison:
if (userInput === secret) { /* … */ }
Behind the scenes, this is a variable‑time operation.
2.Conditional logic on secrets:
if (password.startsWith('admin')) {
// do something slower
} else {
// do something faster
}
Even tiny branches can leak a few microseconds.
3.Hashing without constant‑time checks:
You compute a hash of the password, then do hash === storedHash
. That comparison can leak.
Example: leaking a 4‑digit PIN
1.Attacker sends PIN “0000” → server checks 0 vs stored 0 (✓), then 0 vs stored 0 (✓), then 0 vs stored 0 (✓), then 0 vs stored 1 (✗) → rejects after 4 steps → takes 1.2 ms.
2.Attacker sends “0001” → mismatch on 4th step → also ~1.2 ms.
3.Attacker sends “0010” → mismatch on 3rd step → takes ~0.9 ms.
By comparing 1.2 ms vs 0.9 ms, the attacker knows the first two digits were correct (“00”) and the third was wrong.
In a handful of rounds, all four digits leak. 💦
How to prevent timing attacks in Node.js
- Use constant‑time comparison
Node.js provides a built‑in for this:
import { timingSafeEqual } from 'node:crypto';
const a = Buffer.from(userComputedHash, 'hex');
const b = Buffer.from(storedHash, 'hex');
if (timingSafeEqual(a, b)) {
// They match (no timing leak)
} else {
// They don’t match
}
Why it’s safe: it always walks through every byte of both buffers, doing the same work no matter where the first difference is.
Key point: wrap every secret compare in timingSafeEqual
, even tiny flags or tokens.
- Throttle brute‑force with slow hashes (key‑derivation)
Even if you hide timing leaks, an attacker could still try billions of guesses per second.
To slow them down, use a computationally expensive hash:
import { scrypt } from 'node:crypto';
// on user signup:
const salt = randomBytes(16).toString('hex');
scrypt(password, salt, 64, (err, derivedKey) => {
// store { salt, derivedKey.toString('hex') }
});
// on login:
scrypt(attempt, storedSalt, 64, (err, attemptKey) => {
const a = Buffer.from(attemptKey, 'hex');
const b = Buffer.from(storedDerivedKey, 'hex');
if (timingSafeEqual(a, b)) { /* good */ }
});
Salt: a random per‑user string so identical passwords don’t look the same.
Work factor (N): increases CPU/memory cost. Higher N → slower hashes → better brute‑force resistance.
- Avoid secret‐dependent branching
Any code that does “if secretValue do slow thing, else fast thing” can leak.
Refactor so that both paths take equal time, or better yet, separate secret logic into a constant‑time function.
- Add fixed
response padding
If you have any other early‑exit condition (e.g. “user not found”), add an artificial delay so every failure response takes the same total time:
const FIXED_DELAY = 200; // ms
async function handleLogin(...) {
const start = Date.now();
let ok = false;
// … perform constant‑time compare or simply leave ok=false if user not found
const elapsed = Date.now() - start;
await new Promise(r => setTimeout(r, Math.max(0, FIXED_DELAY - elapsed)));
if (ok) { /* success */ }
else { /* generic “invalid” */ }
}
- Layer on rate‐limiting and account lockouts
Even with slow hashes, attackers can spread attempts across many IPs.
Use tools like express-rate-limit or a firewall to cap attempts per minute, and temporarily lock accounts after repeated failures.
This is your boss after you read this article.👆 lol
Hey! I recently created a tool called express-admin-honeypot.
Feel free to check it out, and if you like it, consider leaving a generous star on my GitHub! 🌟
Let's connect!!: 🤝