← Back to Blog

Checksums Versus Password Hashes: Why MD5 Cannot Serve as Tamper Protection

"Hashing guarantees the file is unchanged" and "hashing stores passwords" use the same function but need security properties in opposite directions. Conflating them causes two distinct failures: treating MD5 as tamper protection, or treating SHA-256 as password storage. This article draws that boundary.

The first question for any hash decision is whether an attacker would deliberately construct input. In non-adversarial settings — file checksums, cache keys, ETags, data sharding — problems come from random factors such as transfer errors or disk faults, and nobody has motivation to build collisions for you. In adversarial settings — tamper protection, signatures, password storage — an attacker has both motive and resources, so the algorithm must assume they will look for weaknesses. This distinction explains why "is MD5 usable" depends entirely on the use case, and why one algorithm can be simultaneously acceptable and unacceptable.

For file verification there are two paths. CRC32 is designed specifically for detecting transfer errors and is one to two orders of magnitude faster than SHA-256, but its collision resistance is weak (32 bits, collision probability grows with the square of file count) — suitable for confirming a download completed intact. SHA-256 is a cryptographic hash with strong collision resistance and no known practical attack, suitable for tamper protection. The in-site hashing tool computes SHA-256 because it suffices for both cases at the cost of speed; CRC32's advantage only matters when validating hundreds of files per second in a build pipeline.

MD5 and SHA-1 need a clear statement. MD5 collisions can now be produced in seconds on an ordinary laptop. SHA-1's SHAttered attack was published in 2017 at roughly 45k dollars, a cost that keeps falling. One technical nuance: a collision attack is not the same as recovering an arbitrary original message, which requires a second-preimage attack and is far harder. But for file replacement, collision is already sufficient — prepare legitimate file A and malicious file B with identical hashes, swap them, and the checksum still matches while the content changed. For anything new, use SHA-256.

Password storage is the critical section. General-purpose hashes are fast, which is precisely what password storage must not be: a GPU computes billions of SHA-256 per second, so testing a leaked hash list against a rainbow table takes minutes. Password hashes must be deliberately slow and satisfy two conditions. First, automatic salting, so identical passwords yield different hashes and one rainbow table fails for every user; the salt need not be secret but must be unique. Second, a tunable cost parameter, so brute force gets more expensive as hardware improves without changing the data format. Argon2id is the preferred choice, with bcrypt, scrypt and PBKDF2 as alternatives that all meet the bar. Never store passwords as plain SHA-256: no salt, no cost parameter, far too fast.

A subtle but serious error is hand-rolling hash(secret + body) for webhook signature verification. This construction has a length-extension weakness: an attacker who does not know the secret can still produce a valid signature over secret || body || padding || extra, appending arbitrary content to a message that still verifies. SHA-256 is collision resistant but not length-extension resistant, while SHA-3 and BLAKE2 are. Use HMAC instead, which addresses this by design through two rounds of mixing rather than simple concatenation. One related rule: compare any token or signature with timingSafeEqual rather than ===, because string equality returns at the first differing character and the timing difference lets an attacker recover the correct value byte by byte.

Migrating legacy hashes does not require a flag day. The standard approach is upgrade on login: verify with the old algorithm, then immediately re-hash with the KDF and store it. The elegance is that only real users trigger the upgrade, so an attacker's stolen hash list is never refreshed. As legitimate users log in, old hashes are replaced naturally. A password strength tool helps assess resistance to cracking and supports policies that reject weak passwords.

Advertisement

Frequently Asked Questions

Is MD5 still usable for verifying file integrity?

**It depends on the goal.** For detecting **accidental corruption** — transfer errors, disk faults, incomplete downloads — MD5 remains adequate: it has not been broken so much as shown to admit constructible collisions, and constructing them requires deliberate effort. File checksums are a non-adversarial setting where nobody builds collisions for you. **For tamper protection**, MD5 is disqualified: an attacker can produce two different files with the same hash and swap in the malicious one while the checksum still matches. SHA-1 is likewise unsafe since the SHAttered attack. **Use SHA-256 for anything new**; it has no known practical collision attack. A hashing tool computes SHA-256 directly.

Why not store passwords as plain SHA-256?

Because **general-purpose hashes are designed to be fast**, which is exactly the property password storage must not have. A GPU computes billions of SHA-256 per second, so testing a leaked list of hashes against a rainbow table takes minutes. Passwords need a **deliberately slow KDF with a salt and a tunable cost parameter**: Argon2id (preferred), bcrypt, scrypt or PBKDF2. These salt internally, so identical passwords produce different hashes and rainbow tables lose their value, and they expose an iteration cost or memory requirement that makes brute force impractical. A bcrypt tool and a password strength tool cover generation and assessment.

Is hand-rolling hash(secret + message) acceptable?

**No — this is among the most common security mistakes.** `hash(secret + message)` is vulnerable to a **length-extension attack**: an attacker who does not know the secret can still produce a valid `hash(secret || message || padding || extra)` and tamper with the appended portion. SHA-256 is collision resistant but not length-extension resistant (SHA-3 and BLAKE2 are). Use **HMAC**, which addresses this by design — `crypto.createHmac('sha256', secret)` in Node. One more rule: compare any token or signature with `timingSafeEqual` rather than plain equality, since timing differences leak information.

← Back to Blog