Storing passwords with MD5, SHA-1, or unkeyed SHA-256 is a critical failure under CWE-916 and OWASP A02 (Cryptographic Failures). These algorithms are designed for speed, not password storage: an attacker who exfiltrates your database can crack billions of candidates per second on commodity hardware. A single database breach becomes a full credential compromise across every account. NIST 800-53 IA-5(1) mandates password-based authenticator storage use salted, one-way, adaptive hashing. Bcrypt, argon2id, or scrypt are the only acceptable choices because their cost factors are tunable to keep brute-force prohibitively expensive even as hardware improves.
Critical because a plaintext or weakly-hashed password database converts a storage breach into an immediate account takeover for every user, with no time window to respond.
Replace any non-adaptive hash with bcrypt (rounds ≥12) or argon2id in your auth library. For bcryptjs:
import bcrypt from 'bcryptjs'
const SALT_ROUNDS = 12
const hash = await bcrypt.hash(plainPassword, SALT_ROUNDS)
const valid = await bcrypt.compare(plainPassword, storedHash)
For new projects prefer argon2id per OWASP 2024: memoryCost: 65536, timeCost: 3. Migrate existing insecure hashes by re-hashing on next successful login and forcing a password reset for inactive accounts after 90 days.
ID: security-hardening.auth-session.password-hashing
Severity: critical
What to look for: Enumerate every location where user passwords are hashed or verified in the codebase. For each location, search for password hashing in auth-related files. Look for imports of bcrypt, bcryptjs, argon2, scrypt, or calls to crypto.scrypt / crypto.pbkdf2. Verify the cost factor (bcrypt rounds ≥10, argon2 memory ≥19456 KB). Flag uses of MD5, SHA-1, SHA-256 (unkeyed), or plain text storage immediately — these are critical failures even if the rest of the code looks correct.
Pass criteria: All password storage uses bcrypt (rounds ≥10), argon2id (memory ≥19456, iterations ≥2), or scrypt (N ≥16384). The hashing is applied before any persistence operation — at least 1 approved algorithm must be used consistently across 100% of hashing locations. Report the count: "X password-hashing locations found, all Y use approved algorithms."
Fail criteria: Passwords are hashed with MD5, SHA-1, SHA-256 (without proper key stretching), stored in plaintext, or no password hashing library is present for a project that handles user passwords.
Skip (N/A) when: The project uses a managed auth service (Clerk, Auth0, Supabase Auth, Firebase Auth) that handles password hashing internally, or the project has no user authentication at all.
Cross-reference: The secrets-not-committed check verifies that password hashes and salt values are not leaked into version control.
Detail on fail: Specify the insecure pattern found. Example: "User passwords hashed with SHA-256 in lib/auth.ts — SHA-256 without key stretching is insecure for password storage" or "No password hashing library detected but a /register route accepts a password field"
Remediation: Replace any non-stretched hash with a proper password hashing algorithm. Bcrypt is the most widely understood option:
import bcrypt from 'bcryptjs'
// Hashing (during registration or password change)
const SALT_ROUNDS = 12
const passwordHash = await bcrypt.hash(plainPassword, SALT_ROUNDS)
// Verification (during login)
const isValid = await bcrypt.compare(plainPassword, storedHash)
if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials' })
}
For new projects, argon2id is the recommended choice per OWASP 2024 guidance:
import argon2 from 'argon2'
const hash = await argon2.hash(plainPassword, {
type: argon2.argon2id,
memoryCost: 65536, // 64 MB
timeCost: 3,
parallelism: 1,
})
Any existing passwords hashed with an insecure algorithm must be migrated: re-hash on next successful login, prompt users to reset if they don't log in within a set window.