Password Hashing: Why SHA-256 Is the Wrong Choice

Published 2026-10-31

The same hash function can be right in one place and wrong in another:

  • Verifying file integrity → must be fast. SHA-256 is correct.
  • Storing user passwords → must be slow. SHA-256 is a liability.

The difference is who the attacker is and how they attack.

Fast hashes help the attacker

If a database leaks and it stored sha256(password), the attacker's job is to guess the input:

Hash Guesses per second on one high-end GPU Time to exhaust an 8-character mixed-case password
SHA-256 ~10^10 days to weeks
bcrypt (cost 12) ~10^4 thousands of years

SHA-256 was designed for throughput — verifying downloads, mining, signatures. GPUs parallelise it extremely well. bcrypt was designed for the opposite: make each evaluation expensive on purpose, so brute force does not scale.

So the problem is not that SHA-256 is weak. The problem is that it is fast, and speed is exactly what the attacker needs.

Salts solve a different problem

A salt defeats precomputation:

Without a salt, 123456 hashes to the same value everywhere. An attacker precomputes tens of millions of common passwords once and looks them up. Two users with the same password also produce the same hash — crack one, get both.

With a per-user random salt, identical passwords produce different hashes and every precomputed table becomes useless.

hash = SHA256(salt + password)

But note: a salt cannot make a fast hash safe. The salt is stored alongside the hash in plaintext, so the attacker simply recomputes per candidate. Salts raise the cost of bulk cracking, not the cost of one guess. To raise the per-guess cost you must make the function slow.

bcrypt: three practical details

Choosing the cost factor

The cost is an exponent: cost 12 means 2^12 = 4096 rounds. Each increment doubles the time.

Cost Approximate time Verdict
10 50–100 ms Minimum acceptable
12 200–400 ms Recommended default
14 1–2 s High-security, login feels slow

Aim for 200–500 ms per verification. Below that the attacker's advantage grows; above ~1 s users notice, and you have built a CPU exhaustion vector — login becomes a denial-of-service target. Rate-limit the endpoint.

The 72-byte limit

bcrypt only reads the first 72 bytes of the password and silently discards the rest.

Two long passphrases can therefore be equivalent. The standard workaround is to pre-hash: SHA-256 the password, Base64-encode the digest (44 characters), then feed that to bcrypt. Do both steps with UTF-8 and no truncation.

Always verify with the library's compare

const hash = await bcrypt.hash(password, 12);
// stored value looks like $2b$12$...

await bcrypt.compare(inputPassword, hash);   // correct

Do not extract the salt and reimplement the pipeline. The cost factor and version ($2a$ vs $2b$ differ subtly) are encoded in the result string.

Also: never compare hashes with ===. String comparison returns early at the first differing byte, and that timing difference leaks information. Use a constant-time comparison (crypto.timingSafeEqual).

Argon2 is the current recommendation

bcrypt's weakness is that it is not memory-hard: it burns CPU but very little memory, which is exactly what GPUs and FPGAs are built for.

Argon2 (winner of the 2015 Password Hashing Competition) requires both time and memory, which removes the parallelism advantage. OWASP's current order of preference:

  1. Argon2id
  2. scrypt
  3. bcrypt (for legacy compatibility)
  4. PBKDF2 (the floor when compliance requires it)

New projects should start with Argon2id. Existing bcrypt hashes do not need an emergency migration — rehash on the next successful login and upgrade silently.

Generating and verifying

To see what a password looks like under cost 12, or to confirm that a stored $2b$12$... value matches a candidate, use bcrypt Generator — it runs locally in the browser.

For generating initial passwords, use Password Generator. Do not roll your own with Math.random(); it is not a cryptographically secure source.

The reverse case: file integrity needs a fast hash

When a vendor publishes a SHA-256 checksum for a download, you compute the digest and compare. This must be fast — a multi-gigabyte file should hash in seconds.

The decision rule is one question: does the data need to stay secret, and must the original be unrecoverable?

Scenario Use Why
User passwords Argon2id / bcrypt cost ≥ 12 Must be slow, resistant to GPUs
File integrity SHA-256 Must be fast
Cache keys, deduplication MD5 is fine Collision risk is irrelevant here
Digital signatures SHA-256 + RSA/ECC The hash only compresses; the signature provides security
Base64 None of the above It is an encoding — see this

The boundary to remember: hashing answers "is this the same thing", encryption answers "can someone else read it". A password hash is neither — it is a hash that is deliberately slow.