# Password hashing uses bcrypt, argon2, or scrypt

- **Pattern:** `ab-002386` (`security-hardening.auth-session.password-hashing`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002386
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002386)

## Why it matters

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.

## Severity rationale

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.

## Remediation

Replace any non-adaptive hash with bcrypt (rounds ≥12) or argon2id in your auth library. For `bcryptjs`:

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

## Detection

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

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

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

## External references

- cwe CWE-916
- owasp A02
- nist IA-5(1)

Taxons: cryptography-and-secrets

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