# Passwords are hashed with a modern algorithm

- **Pattern:** `ab-002222` (`saas-authentication.password-credential.password-hashing-modern`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002222
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002222)

## Why it matters

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.

## Severity rationale

Critical because fast-hashed or plaintext passwords are fully recoverable from a database dump, converting any breach into immediate credential exposure for all users.

## Remediation

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):

```ts
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.

## Detection

- **ID:** `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:

  ```ts
  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.

## External references

- cwe CWE-916
- owasp A02
- nist SP-800-63B §5.1.1.2
- pci-dss Req-8.3.2

Taxons: cryptography-and-secrets

HTML version: https://auditbuffet.com/patterns/ab-002222
