# Password hashes use bcrypt or argon2 (not raw crypto)

- **Pattern:** `ab-000290` (`ai-slop-security-theater.half-wired-crypto.password-hashes-via-bcrypt-or-argon2`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000290
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000290)

## Why it matters

Storing `crypto.createHash('sha256').update(password).digest('hex')` as a password hash is not password hashing — it's a fingerprint. SHA-256 runs at ~3 billion hashes per second on a gaming GPU, making a leaked database crackable in hours for common passwords. CWE-916 (Use of Password Hash With Insufficient Computational Effort) and OWASP A02 describe the requirement: password KDFs (bcrypt, argon2, scrypt) are deliberately slow — 250ms per attempt — which makes brute-force impractical. An AI-generated auth module that uses `crypto.createHash` instead of `bcrypt.hash` is the most common credential-storage mistake in vibe-coded projects.

## Severity rationale

Medium because exploitation requires access to the hashed password database, but once obtained, SHA-256 or MD5 hashes can be cracked at billions of attempts per second on commodity hardware.

## Remediation

Replace any `crypto.createHash` call on passwords with `bcrypt.hash()` or `argon2.hash()`. Both are purposefully slow and include a random salt automatically.

```ts
// Bad: fast, saltless, GPU-friendly
const hash = crypto.createHash('sha256').update(password).digest('hex')

// Good: bcrypt with cost factor 12 (~250ms on modern hardware)
import bcrypt from 'bcrypt'
const hash = await bcrypt.hash(password, 12)

// Verification
const valid = await bcrypt.compare(candidatePassword, storedHash)

// Or argon2 (preferred for new projects)
import argon2 from 'argon2'
const hash = await argon2.hash(password)
const valid = await argon2.verify(storedHash, candidatePassword)
```

If you have existing SHA-256 hashes in production, plan a migration: on next successful login, re-hash with bcrypt and store the new hash.

## Detection

- **ID:** `password-hashes-via-bcrypt-or-argon2`
- **Severity:** `medium`
- **What to look for:** Walk source files for assignments to fields named `password`, `passwordHash`, `password_hash`, `hashedPassword`, `pwHash`. For each assignment, count all value sources and verify each value comes from one of: `bcrypt.hash(`, `bcrypt.hashSync(`, `bcryptjs.hash(`, `argon2.hash(`, `scrypt(` (Node native), `crypto.scrypt(`, `@node-rs/bcrypt`, `@node-rs/argon2`, OR a hosted auth library that handles hashing internally. Count all assignments using insecure alternatives: plain string assignment, `crypto.createHash('md5'`, `crypto.createHash('sha1'`, `crypto.createHash('sha256'` (without scrypt/argon2/bcrypt), or any literal value.
- **Pass criteria:** 100% of password-hash assignments use bcrypt, argon2, scrypt, or a hosted auth library. Report: "X password-hash assignments, Y use a proper KDF, 0 use raw hashing or plain text."
- **Fail criteria:** At least 1 password-hash assignment uses MD5, SHA-1, plain SHA-256, or a literal string.
- **Skip (N/A) when:** No password-hash field assignments found in source code (project doesn't store passwords directly — uses OAuth, magic links, or a hosted auth provider).
- **Cross-reference:** For broader password storage analysis, the Security Hardening audit (`security-hardening`) covers credential storage best practices.
- **Detail on fail:** `"1 insecure password hash: src/lib/auth.ts line 23 stores user.passwordHash = crypto.createHash('sha256').update(password).digest('hex') — SHA-256 is not a password KDF"`
- **Remediation:** Plain SHA-256 / MD5 / SHA-1 are NOT password hashes — they're unsalted, fast, and trivially brute-forceable. Use a real password KDF:

  ```ts
  // Bad: SHA-256 is fast, salt-less, GPU-friendly
  const hash = crypto.createHash('sha256').update(password).digest('hex')

  // Good: bcrypt with appropriate cost factor
  import bcrypt from 'bcrypt'
  const hash = await bcrypt.hash(password, 12)

  // Or argon2
  import argon2 from 'argon2'
  const hash = await argon2.hash(password)
  ```

  Bcrypt's cost factor of 12 takes ~250ms per hash on modern hardware — slow enough to defeat brute force.

---

## External references

- cwe CWE-916
- cwe CWE-759
- owasp A02
- owasp A07

Taxons: cryptography-and-secrets

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