Skip to content
d.devtul.fun
中文
Security · 2026-09-07

MD5 vs SHA-256: What's the Real Difference?

If you have ever written md5(...) and sha256(...) in the same project without thinking about why, this article is for you. They both return a hex string, so they look like the same tool with a different name. Under the hood they are separated by twenty years of cryptanalysis, and one of them is broken.

The side-by-side comparison

PropertyMD5SHA-256
Digest length128 bits (32 hex chars)256 bits (64 hex chars)
Block size512 bits512 bits
Rounds per block6464
Relative speedVery fastFast, roughly 30–50% slower
Collision resistanceBroken (practical)Intact (as of 2026)
Year published19922001

Notice the trap: MD5 is faster than SHA-256. For a checksum that is a nice bonus. For password storage it is a disaster, and we will get to why.

Real collision attacks, not theory

The reason MD5 is retired is not academic caution — it has been defeated in public, twice over.

  • 2004, Wang Xiaoyun. A team in China demonstrated a practical way to generate MD5 collisions in minutes on ordinary hardware. Before that, breaking MD5 was a research curiosity; after that, it was a script.
  • 2005 onward. Researchers produced colliding executable files and colliding X.509 certificates, proving an attacker could get two different documents past an MD5-based signature check.
  • 2017, SHAttered. Google and CWI Amsterdam produced two different PDF files with the same SHA-1 digest, using roughly 9.2 quintillion SHA-1 computations. SHA-1 fell the same way MD5 did.

The takeaway: MD5 collisions and SHA-1 collisions are not "someday". They are downloadable. Any system that trusts either algorithm to tell friend from foe is already compromised.

Why "fast" is a bug for passwords

Here is the part that surprises people. A fast hash is exactly what you do not want when storing passwords.

An attacker who steals your user table does not brute-force one password at a time by hand. They load the hashes into a GPU and try billions of candidates per second. MD5 and SHA-256 are so cheap that a single modern GPU cracks tens of billions of them per second. Your users' "P@ssw0rd!" becomes plaintext before you finish reading this sentence.

Password hashing algorithms (bcrypt, scrypt, Argon2id) are deliberately slow and, in scrypt/Argon2's case, memory-hungry, precisely so that this brute-force math becomes expensive. That is the inversion: for checksums, slow is bad; for passwords, slow is the feature.

When MD5 is still good enough

Saying "MD5 is broken" does not mean "never type md5 again". It means "do not use it where an adversary chooses the inputs". There are plenty of non-adversarial jobs where MD5 is perfectly fine:

  • Deduplication. "Have I already seen this exact blob?" Two identical files giving the same MD5 is all you need, and no attacker is trying to forge a collision against your cache.
  • Cache keys. Turning a complex query into a short key. Collisions would only mean a rare cache miss, not a breach.
  • Accidental corruption checks. Verifying a file did not get truncated in transit. A random bit flip will change the MD5 with overwhelming probability, which is all you are testing for.

If the question is "did this file change by accident?", MD5 answers it. If the question is "can someone fake this file to fool my system?", MD5 lies.

Commands you can run today

Computing a SHA-256 of a file is one line on almost any system:

$ sha256sum report.pdf
a3f1c0...report.pdf

$ # compare against a published checksum
$ sha256sum -c report.pdf.sha256
report.pdf: OK

With OpenSSL the same job looks like this:

$ openssl dgst -sha256 report.pdf
SHA2-256(report.pdf)= a3f1c0...

$ openssl dgst -md5 report.pdf
MD5(report.pdf)= 3e2b9d...

Note how OpenSSL still lets you ask for MD5. The tool is neutral; the responsibility to pick the right algorithm is yours.

A migration strategy that does not break logins

If you have MD5 password hashes in a legacy database, you cannot just rehash everyone at once — you no longer have the plaintext. The standard pattern is to upgrade on the next successful login:

# pseudocode
stored = db.get_hash(user)
if stored.algo == "md5":
    if md5(password) == stored.value:        # verify with the old scheme
        stored = bcrypt.hashpw(password, bcrypt.gensalt(12))
        db.save_hash(user, stored)           # upgrade transparently
        return OK
    else:
        return FAIL
else:
    return bcrypt.checkpw(password, stored)  # normal path

Over a few weeks, active users migrate to the strong scheme automatically, and dormant accounts can be forced to reset. No mass plaintext breach required.

The mistake: rolling your own MAC with MD5

A pattern you will still find in old code is "signing" a message by concatenating a secret and a hash:

# wrong — vulnerable to length-extension, and MD5 is broken anyway
token = md5(secret + message)

# right — use a keyed MAC
import hmac, hashlib
token = hmac.new(secret, message, hashlib.sha256).hexdigest()

The naive version fails twice: MD5 should not be trusted for security, and even with SHA-256, hash(secret || message) is exposed to the length-extension attack we covered earlier. HMAC exists precisely to close that gap. If you are authenticating requests between services, use HMAC-SHA256 from a library and never hand-assemble it.

SHA-512 and BLAKE3: the other two options

SHA-256 is not the only secure choice. SHA-512 produces a 512-bit digest and, because it works on 64-bit words, is often faster than SHA-256 on 64-bit hardware — useful for hashing large files where the extra output size is not a problem. BLAKE3 is the modern contender: it is parallelisable across cores, dramatically faster than SHA-2 on large inputs, and still collision-resistant. The reason SHA-256 remains the default is boring and good — every language and every signing standard supports it, so it is the safe interoperability choice. Reach for SHA-512 or BLAKE3 when you have measured a real performance need, not by default.

Bottom line: use SHA-256 (or SHA-512, or BLAKE3) anywhere security matters, keep MD5 only for the non-adversarial jobs listed above, and treat SHA-1 as retired. The cost of picking right is one line of code and a habit of asking "who chooses the inputs here?" Need to generate both and compare the output lengths yourself? The hash generator does it in the browser, no install needed.

Keep reading