WWeHelpDevs
SecuritySeptember 3, 2026·7 min read

MD5 vs SHA-1 vs SHA-256: Understanding Cryptographic Hashing

Why MD5 and SHA-1 are broken for security purposes, what SHA-256 gets right, and how to pick a hashing algorithm for the job you actually have.

"Just hash it" is one of the most misleading phrases in software engineering, because it glosses over a decision that actually matters: which hash function, and for which purpose. MD5, SHA-1, and SHA-256 are all "hash functions" in the generic sense, but they have very different security properties, and using the wrong one for the wrong job is a real, recurring source of vulnerabilities.

What a Hash Function Actually Guarantees

A cryptographic hash function takes input of any size and produces a fixed-size output (a "digest"), with three properties a secure hash function is supposed to hold:

  • Deterministic — the same input always produces the same output.
  • Pre-image resistant — given a hash, you shouldn't be able to find an input that produces it, short of brute force.
  • Collision resistant — you shouldn't be able to find two different inputs that hash to the same output.

The word "cryptographic" is doing real work in that phrase — MD5 and SHA-1 are still hash functions, but they're no longer considered cryptographically secure hash functions, because practical attacks exist against their collision resistance.

MD5: Broken, Still Everywhere

MD5 produces a 128-bit digest. In 2004, researchers demonstrated practical collision attacks; by 2008, forged SSL certificates were demonstrated using MD5 collisions in the wild. It is not safe for anything security-sensitive — password hashing, digital signatures, certificate generation, or integrity verification against a malicious adversary.

Where MD5 is still fine: non-adversarial use cases where you just need a fast, deterministic fingerprint — deduplicating files, generating a cache key from content, checking for accidental (not malicious) file corruption. Nobody is going to craft a malicious file that collides with your cache key by accident.

// Fine: non-security fingerprinting
const cacheKey = md5(fileContents)

// Not fine: anything where an adversary controls the input
const passwordHash = md5(userPassword) // never do this

SHA-1: Also Broken, Also Everywhere

SHA-1 produces a 160-bit digest and was the standard successor to MD5 for years — used in Git commit hashes, TLS certificates, and countless integrity checks. In 2017, Google and CWI Amsterdam published a practical collision attack ("SHAttered"), producing two different PDFs with identical SHA-1 hashes. Since then, major browsers and CAs have deprecated SHA-1 for certificates, and it should be treated the same as MD5 for any adversarial context.

Git's continued use of SHA-1 for commit hashes is a notable exception worth understanding rather than dismissing: Git's threat model for hash collisions is narrower than a TLS certificate's, and Git has since moved to a transitional collision-detection mode and is migrating toward SHA-256 for new repositories — the fact that a widely-used tool hasn't fully moved off SHA-1 yet doesn't mean it's safe to choose for a new design.

SHA-256: The Current Practical Default

SHA-256 (part of the SHA-2 family) produces a 256-bit digest and has no practical collision attack as of today. It's the right default for integrity verification, digital signatures, and blockchain-style content addressing.

// Compute a SHA-256 hash in the browser, no libraries needed
async function sha256(text) {
  const data = new TextEncoder().encode(text)
  const hashBuffer = await crypto.subtle.digest('SHA-256', data)
  return Array.from(new Uint8Array(hashBuffer))
    .map((b) => b.toString(16).padStart(2, '0'))
    .join('')
}

You can compute SHA-1, SHA-256, and SHA-512 digests for any text directly with our Hash Generator, which runs entirely through the browser's WebCrypto API — nothing you paste is sent anywhere.

The Mistake That Matters Most: Hashing Passwords

This deserves its own section because it's the single most consequential misuse of hash functions, and SHA-256 doesn't fix it either. None of MD5, SHA-1, or plain SHA-256 should ever be used to store passwords directly, because all three are fast — designed to hash gigabytes per second, which is exactly the wrong property for password storage. A fast hash lets an attacker who steals your password database brute-force it at billions of guesses per second on commodity GPUs.

Password storage needs a deliberately slow, memory-hard function designed specifically for this: bcrypt, scrypt, or Argon2 (the current recommended default). These incorporate salting automatically and are tunable to stay slow even as hardware gets faster — a property general-purpose hash functions don't have and were never designed for.

// Wrong — fast general-purpose hash, no salt, brute-forceable at scale
const stored = sha256(password)

// Right — Argon2id via a library, purpose-built for password storage
const stored = await argon2.hash(password)

Summary

  • MD5 and SHA-1 are broken for security purposes (practical collision attacks exist) — fine only for non-adversarial fingerprinting, never for anything security-critical.
  • SHA-256 is the current safe default for integrity checks, digital signatures, and general-purpose secure hashing — use our Hash Generator to compute it for any text.
  • "Cryptographic hash function" and "safe for password storage" are not the same thing — even SHA-256 is too fast for passwords.
  • Use bcrypt, scrypt, or Argon2 for password hashing specifically; they're deliberately slow and memory-hard, which is the property that matters there.
← All articles

Enjoyed this article?

Get new tutorials and guides in your inbox every week. Free, no spam.

Subscribe to WeHelpDevs →