Why Storing Passwords as Hashes Isn't Enough
A plain SHA-256 hash of a password can be cracked in seconds. Salting fixes part of the problem. Slowness fixes the rest. Here's why you need both.
The first rule of password storage: never store the password itself. If your database leaks, plaintext passwords mean every account is immediately compromised — and most users reuse passwords across sites, so the damage spreads.
Storing a hash is better. Verify by hashing the login attempt and comparing. But "better" isn't the same as "good," and plain hashing has two specific weaknesses that a motivated attacker will exploit.
Weakness 1: lookup tables
Hashing is deterministic. SHA-256 of "password123" is always ef92b778..., on every machine, in every language. Attackers know this. Before your database even exists, someone has precomputed SHA-256 hashes for millions of common passwords and built a lookup table. Your breach dumps those hashes, the attacker queries the table, and accounts cracked within hours.
MD5 is worse: the tables are bigger because MD5 has been around longer and runs faster. Try pasting the MD5 hash of any common word into Google — you'll often get the original back as a top result.
Salting: the fix for lookup tables
A salt is a random value generated fresh for each password and stored alongside the hash. You hash the combination of salt + password, not the password alone.
Same password, two users, two different salts: you get two different hashes. An attacker can't build a table in advance because the salt is unpredictable. They'd need a separate table for each salt — which means a separate brute-force for each account. That scales badly.
The salt is not a secret. It's stored in plaintext in your database. The security comes from uniqueness, not secrecy.
Weakness 2: speed
Salting solves the precomputation problem. It doesn't solve the speed problem.
SHA-256 is designed to be fast. A commodity RTX 4090 can compute roughly 20 billion SHA-256 hashes per second. An attacker with your salted database and a list of 10 million common passwords still only needs 0.5ms to try all of them against a single account. A longer list of a billion passwords takes about 50ms per account. That's still fast enough to be a real problem for weak passwords.
The fundamental issue: SHA-256 was built for speed. Password verification needs slowness.
What actually belongs in password storage
Functions designed specifically for passwords: bcrypt, scrypt, and Argon2id. They're slow by design, include salting automatically, and expose a cost parameter you can increase as hardware improves. bcrypt at cost 12 takes ~250ms per hash on a modern server. That's imperceptible to a user but makes brute-forcing 10 million passwords take three months per account rather than milliseconds.
The lesson structure: use SHA-256 for integrity checking. Never use it for passwords. We cover bcrypt and Argon2 in Lesson 3 and how attackers exploit speed in Lesson 3 as well.
If you've inherited a codebase using md5($password) or sha256($password) — that's a live vulnerability. Prioritise re-hashing with bcrypt.