Passwords hashed with MD5, SHA-1, or unsalted SHA-256 can be brute-forced at billions of attempts per second on commodity GPU hardware. CWE-916 (Use of Password Hash With Insufficient Computational Effort) applies directly. A database breach of MD5-hashed passwords is functionally equivalent to a plaintext breach for any user with a common password. NIST SP 800-63B §5.1.1.2 (Memorized Secret Verifiers) mandates that verifiers store memorized secrets using a suitable one-way key derivation function, and OWASP A02 (Cryptographic Failures) covers the broader category. PCI-DSS v4.0 requires strong cryptography for authentication factors at rest. A cost factor below 10 for bcrypt provides insufficient slowdown on modern hardware.
Critical because fast-hashed or plaintext passwords are fully recoverable from a database dump, converting any breach into immediate credential exposure for all users.
Replace any general-purpose hash with bcrypt, scrypt, or argon2. Set the cost factor to at least 10 for bcrypt (12 is preferred on modern servers):
import bcrypt from 'bcryptjs'
// Registration / password change
const ROUNDS = 12
const hash = await bcrypt.hash(password, ROUNDS)
await db.user.create({ data: { email, passwordHash: hash } })
// Login — always use the library's compare, never a raw equality check
const isValid = await bcrypt.compare(candidatePassword, storedHash)
For new projects, argon2id is the current recommendation from the OWASP Password Storage Cheat Sheet. If migrating from a weaker hash, implement a rehash-on-login strategy: if the stored hash is MD5/SHA format, verify against the old hash, then immediately replace it with a bcrypt hash and save.
ID: saas-authentication.password-credential.password-hashing-modern
Severity: critical
What to look for: Find where passwords are hashed in the codebase — typically in the user registration handler, password change handler, and the password verification function. Check which hashing library is used: bcrypt (bcryptjs, bcrypt), scrypt (crypto.scrypt), argon2 (argon2), or PBKDF2 (crypto.pbkdf2). Verify it is NOT MD5, SHA-1, SHA-256, or any other general-purpose hash function used directly without a work factor. Before evaluating, extract and quote the password hashing configuration — the algorithm name, cost factor, and the code line where hashing occurs. Count all instances found and enumerate each.
Pass criteria: Passwords are hashed using bcrypt (cost factor >= 10), scrypt, argon2 (any variant), or PBKDF2 (iterations >= 100,000). The hash is stored and compared using the library's own compare function (not a raw equality check). At least 1 implementation must be confirmed.
Fail criteria: Passwords are hashed with MD5, SHA-1, SHA-256, SHA-512, or any other non-password-specific hash function. Passwords are stored in plaintext. The cost factor for bcrypt is set to less than 10. Do NOT pass if passwords are hashed with MD5, SHA-1, or unsalted SHA-256 — these are not acceptable for password storage regardless of other mitigations.
Skip (N/A) when: No local password authentication — OAuth/social auth only, or a managed auth provider (Clerk, Firebase Auth, Auth0) handles all password storage. Signal: no password field in the user schema, no hashing code found.
Cross-reference: The password-strength-enforced check verifies the input requirements that protect these hashed credentials from brute-force attacks.
Detail on fail: "User registration at /api/auth/register hashes passwords with SHA-256 (crypto.createHash) rather than bcrypt or argon2" or "bcrypt.hash() called with cost factor 8 — should be at least 10".
Remediation: Fast hashing algorithms (MD5, SHA-256) can be brute-forced at billions of attempts per second with modern GPU hardware. Password-specific algorithms with a work factor slow this to thousands of attempts per second:
import bcrypt from 'bcryptjs'
// Hash on registration/password change
const ROUNDS = 12 // 10 is minimum; 12 is comfortable for most servers
const hash = await bcrypt.hash(password, ROUNDS)
// Compare on login (timing-safe)
const isValid = await bcrypt.compare(candidatePassword, storedHash)
For new projects, argon2id is the modern recommendation if your runtime supports it. If your auth provider handles passwords, verify in their documentation that they use a suitable algorithm.