Timing attacks: recovering a secret from response times
An early-exit comparison leaks how many leading characters of a guess are correct.
A naive comparison returns at the first mismatching byte. The time to reject a guess grows with the length of its correct prefix, so measuring it recovers the secret one position at a time. For a 4-character alphanumeric secret, that is at most 4 × 36 guesses instead of 36⁴.
The demo exaggerates the difference so it is visible.
Response time per guess. The slowest one matched one more character.
for i := range given { if given[i] != secret[i] { return false } } // exits earlyIn practice
The real difference is a few nanoseconds per byte, while network jitter is orders of magnitude larger. Attackers average many samples per guess and move closer to the target, for example into the same cloud region, to reduce noise. Brumley and Boneh showed remote timing attacks against OpenSSL in 2003.
Typical targets
- API keys and bearer tokens compared with
==. - HMAC signatures on webhooks checked with a string comparison.
- Password reset tokens and OTP codes.
- Logins that skip password hashing for unknown users. That one leaks which accounts exist rather than a secret.
If a password can be compared byte by byte, it is stored in plaintext. Fix that first.
Fix
Compare secrets in constant time:
if !hmac.Equal(gotMAC, expectedMAC) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
} subtle.ConstantTimeCompare works on any byte slices but returns immediately when the lengths differ. Comparing fixed-length values (hashes, MACs) avoids leaking the length.
Store passwords with Argon2id or bcrypt. For unknown users, hash a dummy password so both paths take the same time. Rate-limit login and code verification.
I test web apps, APIs and LLM integrations. Contact.
Next article → SQL injection: authentication bypass with a single quote