You verify an API token the obvious way:
if (providedToken === storedToken) grantAccess();That === is a side channel. String comparison stops at the first differing character, so a token guess that gets the first byte right takes a hair longer to reject than one that gets it wrong. An attacker who can measure response time submits a..., b..., c..., finds that one branch is consistently slower, learns the first byte, then repeats for the second — extracting the whole secret one byte at a time. A 32-character token that would take longer than the universe to brute force falls in a few thousand timed requests. This is a timing attack, and the fix is to compare secrets in a way that always takes the same amount of time.
Where the leak lives
Anywhere you compare a secret with ==, ===, .equals(), or memcmp:
- API key / token verification on every authenticated request
- HMAC / webhook signature checks (
signature === expected) - Password reset and email-verification tokens
- Coupon, license, and CSRF token validation
- Password checks that compare raw values instead of verifying a hash (which has its own constant-time compare inside)
The tell is a short-circuiting comparison whose runtime depends on how much of the input matched.
The fix: constant-time comparison
Constant-time comparison inspects every byte regardless of where a mismatch occurs and folds the differences together, so the duration is independent of the inputs' contents. Never hand-roll it — the compiler may optimize your loop back into an early return. Use your platform's crypto primitive.
The principles
- Compare secrets in constant time. Use
timingSafeEqual/compare_digest/hash_equalsfor tokens, keys, and signatures — anything where a match grants access. - Verify passwords with a slow hash, never a direct compare.
bcrypt.checkpw/password_verifyhandle the constant-time step for you. - Hash before comparing when lengths might differ, so the length itself doesn't leak and the constant-time primitive's equal-length requirement is met.
- Add rate limiting as defense in depth. Timing attacks need many samples; throttling and lockouts raise the cost.
The one line: if a comparison decides access, its running time must not depend on the secret — use the constant-time primitive your crypto library ships.
Try it in the timing attack simulation: recover a token byte by byte from response times, then switch to constant-time comparison and watch the signal vanish. It complements the HMAC signatures guide, where the same constant-time rule applies to signature checks.
Frequently Asked Questions
Related posts
Padding Oracle Attacks: Decrypting Without the Key
A padding oracle lets an attacker decrypt CBC ciphertext one byte at a time — without ever knowing the key — just from whether the server says 'bad padding.' Here's how the attack works and why authenticated encryption ends it.
Jul 25, 2026 · 7 min readEC Keys and ECDSA: The Faster Alternative to RSA for JWT Signing
Learn how elliptic curve keys work, why P-256 is the right default for JWT ES256, and how ECDSA compares to RSA in size, speed, and security.
Mar 13, 2026 · 6 min readAES vs RSA — Which Encryption Should You Use in Your App?
A practical guide for backend developers on when to use AES symmetric encryption vs RSA asymmetric encryption. Includes real-world use cases, code examples in JavaScript, Python, Java, PHP, Kotlin, and Swift.
Mar 12, 2026 · 10 min read