I audited a legacy PHP app last year and found 400,000 user passwords stored as plain MD5 hashes β no salt, no iterations. A dictionary attack against that database would crack the majority of accounts in under an hour using a cheap GPU instance. The team thought MD5 was βencrypted.β It is not. But whether MD5 is dangerous depends entirely on what you are using it for.
Generate hashes locally β any algorithm.
MD5, SHA-256, SHA-512 β computed in your browser. local processing. Your data stays on your device.
Open Hash Generator βWhat MD5 Actually Is
MD5 is a cryptographic hash function designed by Ron Rivest in 1991. It takes any input β a password, a file, a string β and produces a fixed 128-bit (32 hex character) output. The same input always produces the same hash. Different inputs should (in theory) produce different hashes.
βShouldβ is the problem. In 2004, Xiaoyun Wang and Hongbo Yu published a paper demonstrating a practical collision attack against MD5 β two different inputs that produce the same 128-bit output. By 2008, researchers at CWI Amsterdam had used that technique to forge a valid SSL certificate. The attack made front-page security news and MD5 has been deprecated for cryptographic uses since.
Why MD5 Is Dangerous for Passwords
MD5 was designed for speed. That is exactly the wrong property for password hashing.
A single NVIDIA RTX 4090 can compute approximately164 billion MD5 hashes per second(measured with Hashcat). Against an unsalted MD5 hash database:
| Password Type | Crack Time (RTX 4090, MD5) | Crack Time (RTX 4090, bcrypt cost 12) |
|---|---|---|
| 6-char alphanumeric | Instant (precomputed) | 3 weeks |
| 8-char alphanumeric | Under 1 minute | 200 years |
| 8-char with symbols | ~2 hours | Millions of years |
| 12-char random | Days to weeks | Heat death of universe |
bcrypt's βcost factorβ controls how many internal rounds it runs. At cost 12, each bcrypt computation takes roughly 300ms β the same GPU that cracks MD5 at 164 billion/second is limited to around 1,800 bcrypt hashes per second. That 90-million-fold slowdown is the entire point.
The Collision Attack β What It Actually Means
A collision means two different inputs hash to the same output. For password hashing, this is less of a concern than raw speed β an attacker is not trying to find a collision, they are just computing hashes of common passwords until one matches your stored hash.
Where collisions are catastrophic is in digital signatures and certificate verification. The 2008 rogue CA certificate attackexploited MD5 collisions to create a fraudulent certificate authority that browsers would trust. That attack is what forced the CA/Browser Forum to ban MD5 in SSL certificates.
Where MD5 Is Still Fine
MD5 remains appropriate in situations where there is no adversary trying to craft a malicious input β specifically, non-cryptographic integrity checks.
File corruption detection: Checking that a file downloaded intact from a known-good source. You are not defending against an attacker β you are verifying that the network did not garble bytes in transit. MD5 catches that reliably.
Database deduplication: Detecting duplicate records in a dataset. If two rows produce the same MD5, they are almost certainly identical. The theoretical collision risk at any realistic dataset size is negligible for this purpose.
Cache keys: Generating short, deterministic identifiers for cache entries in systems you control. No adversary is crafting collisions against your Redis cache keys.
| Use Case | MD5 Safe? | Use Instead |
|---|---|---|
| Password storage | No β ever | bcrypt, scrypt, Argon2id |
| Digital signatures | No | SHA-256 or SHA-3 |
| SSL / TLS certificates | Banned | SHA-256 (required by CA/B Forum) |
| HMAC / API authentication | No | HMAC-SHA256 |
| File download integrity check | Yes (non-adversarial) | SHA-256 preferred if available |
| Deduplication / cache keys | Yes | xxHash or MD5 β both fine here |
What Correct Password Hashing Looks Like
import bcrypt from 'bcryptjs';
// STORING a password
const COST_FACTOR = 12; // ~300ms on modern hardware β adjust based on your server
const hash = await bcrypt.hash(plaintextPassword, COST_FACTOR);
// Store 'hash' in your database β never store plaintext or MD5
// VERIFYING a password at login
const isValid = await bcrypt.compare(plaintextPassword, storedHash);
// Returns true/false β timing-safe by default in bcryptjs
// BAD β never do this
const badHash = require('crypto')
.createHash('md5')
.update(plaintextPassword)
.digest('hex');
// This is what 400,000 accounts in that PHP app looked like// For Python (Django and Flask both default to PBKDF2 + SHA256) from argon2 import PasswordHasher ph = PasswordHasher(time_cost=2, memory_cost=65536, parallelism=2) # Store hash = ph.hash(plaintext_password) # Verify (raises VerifyMismatchError if wrong) ph.verify(hash, plaintext_password)
Common Mistakes When Migrating Away from MD5
Missing Salt β Still Broken
Adding a salt does not fix MD5's speed problem β it only defeats precomputed rainbow tables. A salted MD5 still runs at 164 billion computations/second on that GPU. The attacker just has to brute-force each hash individually rather than looking it up. For long random passwords, this is slow. For common passwords, it is still fast.
Salt prevents rainbow tables. It does not prevent brute force. Only a slow algorithm prevents brute force.
Safe Migration: Rehash on Next Login
You cannot convert existing MD5 hashes to bcrypt because you do not have the original passwords. The right migration pattern is to rehash passwords as users log in:
async function login(plaintext: string, storedHash: string) {
// Detect if this is an old MD5 hash (32 hex chars, no bcrypt prefix)
const isMD5 = /^[a-f0-9]{32}$/.test(storedHash);
if (isMD5) {
// Verify the old MD5 hash (temporary compatibility)
const md5 = require('crypto').createHash('md5').update(plaintext).digest('hex');
if (md5 !== storedHash) return false;
// Immediately upgrade to bcrypt on successful login
const newHash = await bcrypt.hash(plaintext, 12);
await db.users.updateHash(userId, newHash);
return true;
}
// Standard bcrypt path for already-migrated users
return bcrypt.compare(plaintext, storedHash);
}SHA-256 Is Not a Password Hashing Algorithm
SHA-256 is far more secure than MD5 for signatures and certificate work. But it is still too fast for passwords β around 10 billion SHA-256 hashes per second on the same GPU. Do not substitute SHA-256 for bcrypt or Argon2 when storing passwords. They are different tools solving different problems.
My Recommendation
If you are storing passwords: use Argon2id. It is theOWASP-recommendedfirst choice as of 2023. If Argon2 is not available in your stack, bcrypt with cost 12 is fine. If you are on a legacy system you cannot change, add salt and upgrade accounts on next login.
If you are using MD5 for checksums or dedup: it is fine. Just make sure there is no attacker in that pipeline. The moment you are checking trust boundaries β authentication, token validation, signature verification β drop it.
For generating hashes locally without sending data to a server, try our offline hash generator. And if you are rethinking your overall security approach, our password security guide covers the full stack β from storage algorithms to breach notification handling.
Hash files or strings locally.
MD5, SHA-256, SHA-512 β all computed in-browser. Nothing leaves your device.
Open Hash Generator β