Try the Hash Generator

Why Comparing Hashes With == Leaks Your Secret

Comparing a signature with === leaks it one byte at a time, and hash(secret + message) can be forged without the secret. Two one-line fixes for both.

September 17, 2026 7 min read
Share: Facebook WhatsApp LinkedIn Email
Why Comparing Hashes With == Leaks Your Secret

The Comparison That Leaks

Your API verifies a webhook signature. The client sends a hash; you compute the expected hash and compare:

if (receivedSignature === expectedSignature) {
  processWebhook();
}

Correct output, every time. Also potentially vulnerable, because that comparison doesn't take the same amount of time for every input.

String equality in most languages short-circuits — it compares byte by byte and returns false at the first mismatch. A signature differing in the first byte fails faster than one differing in the twentieth.

An attacker who can measure that difference can determine the correct signature one byte at a time. That converts an infeasible brute force over the whole hash into a linear search: a few hundred guesses per byte rather than astronomically many for the whole value.

How the Attack Works

Suppose the expected signature is 64 hex characters. An attacker:

  1. Sends a request with the first character set to 0, the rest arbitrary. Times the response.
  2. Repeats for 1 through f.
  3. One of those 16 takes marginally longer, because the comparison got one byte further before failing.
  4. Fixes that character, moves to the second.
  5. Repeats.

Sixteen guesses per character, 64 characters — around 1,024 requests to recover the full signature, versus 16^64 for a blind brute force.

The timing difference per byte is tiny, on the order of nanoseconds. In practice the attacker averages over many samples to pull the signal out of network jitter, which raises the request count substantially but doesn't change the fundamental feasibility.

Whether this is exploitable depends heavily on context. Over the open internet, network noise makes it difficult and slow. On a local network, in a shared hosting environment, or against a co-located service, it becomes considerably more practical. Research has demonstrated remote timing attacks against real systems.

The pragmatic position: constant-time comparison costs you nothing, so there's no reason to accept the risk.

Constant-Time Comparison

The fix is a comparison that always examines every byte, regardless of where mismatches occur.

The technique is to XOR each pair of bytes and OR the results together, then check whether the accumulated value is zero:

result = 0
for i in 0..length:
    result |= a[i] XOR b[i]
return result == 0

Every byte is processed. There's no early exit. The runtime depends only on the length, not on the content.

Use your language's built-in function rather than writing this yourself:

  • Node.js: crypto.timingSafeEqual(a, b)
  • Python: hmac.compare_digest(a, b)
  • PHP: hash_equals($known, $user)
  • Go: subtle.ConstantTimeCompare(a, b)
  • Java: MessageDigest.isEqual(a, b)
  • Ruby: OpenSSL.secure_compare(a, b)
  • .NET: CryptographicOperations.FixedTimeEquals(a, b)

A caveat on argument order: some implementations, including PHP's hash_equals, expect the known-correct value first. Getting it backwards can leak length information in some implementations. Read the documentation for yours.

Length differences still leak. Most constant-time functions return early or throw if the lengths differ, revealing the expected length. That's usually acceptable — the length of a hash is public knowledge — but it's worth knowing.

HMAC: Why Hashing With a Key Is Not Straightforward

If you need to verify that a message came from someone who holds a shared secret, the obvious construction is to hash the secret concatenated with the message:

signature = SHA256(secret + message)

This is broken for SHA-1, SHA-2 and MD5, because of a property called length extension.

These hash functions work by processing input in blocks and carrying an internal state forward. The final output is that internal state. Which means an attacker who has SHA256(secret + message) can resume the computation from that state and compute SHA256(secret + message + padding + extra) — a valid signature for a message they've extended — without knowing the secret at all.

The practical impact is real. An attacker who intercepts a signed API request can append parameters to it and produce a valid signature for the modified request.

HMAC solves this properly

HMAC is a specific construction designed around the problem:

HMAC(K, m) = H((K ⊕ opad) ‖ H((K ⊕ ipad) ‖ m))

Two nested hash operations with two derived keys. Because the outer hash's input is the fixed-length output of the inner hash, length extension gains an attacker nothing.

Always use your library's HMAC function. Never construct keyed hashes by concatenation.

  • Node.js: crypto.createHmac('sha256', key)
  • Python: hmac.new(key, msg, hashlib.sha256)
  • PHP: hash_hmac('sha256', $data, $key)

SHA-3 and BLAKE2 are not vulnerable to length extension due to their different internal structure, so SHA3(secret + message) is safe in a way that SHA256(secret + message) isn't. But HMAC works with all of them and requires no reasoning about which is which — use it regardless.

Generating and Comparing Hashes

The Hash Generator computes MD5, SHA-1, SHA-256 and SHA-512 for any input:

  1. Enter your text.
  2. Select the algorithm.
  3. Read the hash.

Useful for verifying that your implementation produces what you expect — compute a hash of a known test string in the tool and compare it to what your code generates. A mismatch usually means an encoding difference (UTF-8 versus UTF-16, or a trailing newline) rather than an algorithm problem.

Never paste secrets, keys, or production data into any browser tool, including this one. Use test values.

Where Else Timing Matters

Signature verification is the clearest case, but the same principle applies elsewhere.

API key lookup. Comparing a supplied API key against a stored one is the same problem. Use constant-time comparison, and consider storing a hash of the key rather than the key itself.

Password verification. Handled for you if you use a proper password hashing library — bcrypt, Argon2, scrypt all include constant-time comparison internally. Don't compare hashes yourself.

Session token validation. Same category. Constant-time comparison.

Early returns based on secret data. A subtler version: if your code returns "user not found" faster than "wrong password", you've leaked whether an account exists. Some systems deliberately perform a dummy password hash for nonexistent users to equalise the timing.

Cache-based timing. Beyond string comparison, whether data is in cache affects timing. Fully mitigating this is genuinely hard and mostly relevant to cryptographic library authors rather than application developers.

Practical Tips

Use HMAC, never hash(secret + message). No exceptions, no reasoning about which algorithms are safe.

Use your platform's constant-time comparison function. Don't write your own; subtle compiler optimisations can reintroduce early exit.

Check argument order in functions like PHP's hash_equals where known and supplied values have defined positions.

Don't roll your own signature scheme. JWT, HMAC-signed webhooks and similar patterns are well-specified. Deviating from them is where vulnerabilities appear.

Include a timestamp in signed payloads and reject old ones. A valid signature that never expires is a replay vulnerability regardless of how well you compare it.

Sign the whole payload, including anything security-relevant. Signing only part of a request lets an attacker modify the unsigned part freely.

FAQ

Is a timing attack realistic over the internet? Harder than on a local network, but demonstrated in research. Since constant-time comparison is free, there's no reason to leave the exposure open.

What is a length extension attack? An attacker with H(secret + message) can compute a valid hash for an extended message without knowing the secret, exploiting how SHA-2 and similar functions carry internal state.

Is SHA-256 vulnerable to length extension? Yes. So are SHA-1, SHA-512 and MD5. SHA-3 and BLAKE2 are not, due to different internal construction.

Should I use HMAC-SHA256 or SHA3? HMAC-SHA256 is widely supported and secure. SHA-3 in a plain keyed construction is also safe, but HMAC is the more portable choice.

Do I need constant-time comparison for password hashes? Your password hashing library handles it. Don't compare hashes manually.

Does === in JavaScript short-circuit? Yes, string comparison compares character by character and returns at the first difference. Use crypto.timingSafeEqual on buffers for security-sensitive comparisons.

The Takeaway

Two mistakes account for most hash-related vulnerabilities in application code: comparing secrets with ordinary equality, and building keyed hashes by concatenation. Both have one-line fixes that are already in your standard library — timingSafeEqual and hmac. Using them costs nothing and closes both classes of problem.

Generate MD5, SHA-1, SHA-256 and SHA-512 hashes free with the Hash Generator at sadiqbd.com — no sign-up, instant results.

Ask AI about this article
Share: Facebook WhatsApp LinkedIn Email

Hash Generator

Free, instant results — no sign-up required.

Open Hash Generator →
Similar Tools
Color Converter REST API Checker HTML Entities Random String Generator JWT Decoder Bcrypt Generator Timestamp Converter UUID Generator
What Developers Still Get Wrong About Password Storage in 2024
Developer
What Developers Still Get Wrong About Password Storage in 2024
How File Hash Verification Actually Works: Determinism, the Avalanche Effect, and What "Match" Really Means
Developer
How File Hash Verification Actually Works: Determinism, the Avalanche Effect, and What "Match" Really Means
SHA-3 Exists Alongside SHA-2, Not Instead of It — The Architecture, Length Extension Vulnerability, and 2024 Usage Guide
Developer
SHA-3 Exists Alongside SHA-2, Not Instead of It — The Architecture, Length Extension Vulnerability, and 2024 Usage Guide
SHA-256 Is Wrong for Passwords and bcrypt Is Wrong for File Integrity — Why Hash Function Choice Depends on Context
Developer
SHA-256 Is Wrong for Passwords and bcrypt Is Wrong for File Integrity — Why Hash Function Choice Depends on Context