Description
MD5 and SHA-1 are unsuitable where collision resistance is required. Password storage needs a dedicated password-hashing function with a salt and an adjustable work factor, rather than these fast hashes. An unkeyed hash also cannot authenticate a download link or token: attackers can change its contents and recompute the hash without finding a collision.
Potential impact
- Authentication bypass or forgery: Collisions can defeat integrity checks that rely on a weak hash, depending on the verification scheme.
- Password disclosure: Fast, unsalted hashes make large-scale guessing and precomputed lookup attacks easier.
- Weakened integrity: Modified data may be accepted as authentic.
- Forged links or tokens: An attacker can recompute an unkeyed hash after altering the data.
Remediation
- Password storage: Prefer Argon2id, or use an appropriate bcrypt, scrypt, or PBKDF2-HMAC-SHA256 configuration. Use random salts and sufficient computational or memory cost, and compare derived values with a constant-time function such as
hmac.compare_digest. For PBKDF2-HMAC-SHA256, use at least 600,000 iterations and a random salt of at least 16 bytes; tune the cost for the service's capacity. - Integrity and authentication: Use a keyed MAC such as HMAC-SHA256 when authenticity is required. Retire MD5 and SHA-1 from security-sensitive uses; use SHA-256/512 or SHA-3 where an unkeyed digest is appropriate.
- Migration: Rehash passwords at login and store the algorithm and parameters with each hash so they can be upgraded.
Examples
Before
python
import hashlib
# Weak MD5/SHA-1 use for passwords and tokens
def store_password_md5(username: str, password: str) -> str:
# BAD: fast MD5 password hashing without a salt
digest = hashlib.md5(password.encode("utf-8")).hexdigest()
return f"{username}:{digest}"
def sign_download_link_sha1(user_id: str, expires: int) -> str:
# BAD: an unkeyed SHA-1 digest does not authenticate the data
data = f"{user_id}:{expires}".encode()
sig = hashlib.sha1(data).hexdigest()
return f"{user_id}:{expires}:{sig}"
After
python
import os
import hmac
import base64
import hashlib
import secrets
# Password storage with PBKDF2-HMAC-SHA256
def hash_password(password: str) -> str:
salt = os.urandom(16)
iterations = 600_000
dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, iterations)
return f"pbkdf2_sha256${iterations}${base64.b64encode(salt).decode()}${base64.b64encode(dk).decode()}"
def verify_password(password: str, stored: str) -> bool:
scheme, iters_s, b64_salt, b64_dk = stored.split("$")
if scheme != "pbkdf2_sha256":
return False
salt = base64.b64decode(b64_salt)
iterations = int(iters_s)
true_dk = base64.b64decode(b64_dk)
test_dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, iterations)
return hmac.compare_digest(true_dk, test_dk)
# Integrity/authentication with HMAC-SHA256 (requires a secret key)
def sign_download_link(user_id: str, expires: int, secret_key: bytes) -> str:
# Generate secret_key with secrets.token_bytes(32), for example, and store it securely
msg = f"{user_id}:{expires}".encode()
mac = hmac.new(secret_key, msg, hashlib.sha256).hexdigest()
return f"{user_id}:{expires}:{mac}"
Explanation:
- Before: Fast, unsalted MD5 makes password guessing easier. The unkeyed SHA-1 link hash can be recalculated after changing the link's contents.
- After: PBKDF2-HMAC-SHA256 uses 600,000 iterations and a random salt to increase guessing cost;
hmac.compare_digestreduces timing leakage in comparison. HMAC-SHA256 authenticates link data using a secret key that must be protected.