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.
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.
Replace any crypto.createHash call on passwords with bcrypt.hash() or argon2.hash(). Both are purposefully slow and include a random salt automatically.
// 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.
ID: ai-slop-security-theater.half-wired-crypto.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:
// 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.