Password Mismanagement
Also known as: Insecure password storage, Weak password hashing, Plaintext passwords, Weak password policy
A family of flaws in which an application stores passwords in plaintext or with a fast, unsalted hash, or enforces a weak policy, so a single database leak turns into mass account takeover; it is prevented by slow salted password hashing, breached-password screening and rate limiting with MFA.
How it works
Password mismanagement is a set of flaws in how an application stores, checks and governs user passwords. The most serious is storing them in plaintext or in a form that can be reversed. Almost as bad is hashing them with a general-purpose fast function, such as MD5, SHA-1 or a bare SHA-256, with no per-user salt and no work factor.
The root cause is using the wrong primitive. General-purpose hashes are designed to be fast, which is exactly what an offline attacker with a stolen table wants. Without a unique salt, identical passwords produce identical hashes, so one effort covers every user who chose the same value. A leak of the table is then a leak of most of the passwords.
Policy flaws compound this: short minimum lengths, forced composition rules that push users toward predictable patterns, mandatory rotation that encourages small edits, no screening against known breached passwords, and no rate limiting or lockout on the login endpoint. Insecure reset flows and passwords written to logs create further exposure.
The fix is a password-specific design. Store only the output of a slow, salted, memory-hard password hashing function (Argon2id, scrypt or bcrypt) with a tuned work factor, never plaintext or encrypted values. Follow NIST 800-63B for policy: favour length, screen against breached lists, drop composition and routine rotation rules. Add rate limiting and MFA so a guessed or reused password alone is not enough.
Walk through it
- 1Read the credential storage code
- 2Name the weakness
- 3Pick the lasting fix
- Review the password policy
- Migrate hashes, add controls and verify
A pre-release review flags the sign-up and login handlers of the rewards site. Read how a password is turned into the value stored in the users table, and note anything a reviewer should question.
1import { createHash } from "crypto";2 3export function storePassword(plain: string): string {4 return createHash("md5").update(plain).digest("hex");5}6 7export async function login(email: string, plain: string) {8 const user = await db.users.findByEmail(email);9 return user && user.password_hash === storePassword(plain);10}Spot it
- Password columns that are plaintext, reversibly encrypted, or short fixed-length hex strings typical of MD5, SHA-1 or SHA-256.
- Identical stored values for different users, which indicates no per-user salt.
- Login endpoints with no throttling or lockout, showing large failed-login volumes across many accounts from few sources.
- Password or reset-token values appearing in application logs, error messages or analytics events.
- No screening of new passwords against breached lists, and policies that force composition rules or routine rotation.
Authentication log
ts=2026-10-11T08:15:02Z event=login_failed user=member-1042 src=198.51.100.7 throttle=none
ts=2026-10-11T08:15:02Z event=login_failed user=member-1043 src=198.51.100.7 throttle=noneApplication log
ts=2026-10-11T08:20:41Z level=debug msg="signup payload" fields=email,password_present=true password_logged=truesqlSQL: duplicate stored password values (no per-user salt)
SELECT password_hash, COUNT(*) AS users
FROM users
GROUP BY password_hash
HAVING COUNT(*) > 1
ORDER BY users DESC;Run on a copy or as a count only. Duplicate values mean no unique salt; never export the values.
grepgrep: fast hashes used on password data in source
grep -rnEi "(md5|sha1|sha256).{0,60}(password|passwd|pwd)" --include=*.ts --include=*.js --include=*.py --include=*.java src/A review aid, expect false positives; confirm each hit is on password handling.
Stop it
Hash passwords with a slow, salted password KDF (Argon2id, scrypt or bcrypt)
Use a maintained library, a unique random salt per user and a work factor tuned to your hardware. Never store plaintext or reversibly encrypted passwords, and never use a fast general-purpose hash for this purpose.
Screen new passwords and follow length-based policy
Check candidates against a breached-password list, using a k-anonymity service such as Have I Been Pwned so the full password never leaves your system. Favour length, allow long passphrases and paste, and drop forced composition and routine rotation, as NIST SP 800-63B advises.
Add rate limiting, MFA and safe reset flows
Throttle or lock repeated failures per account and per source, require MFA for sensitive accounts, use single-use short-lived reset tokens, and keep passwords and tokens out of logs.
Replace the fast hash with Argon2id
Vulnerable
import { createHash } from "crypto";
const stored = createHash("md5").update(plain).digest("hex");Hardened
import argon2 from "argon2";
const stored = await argon2.hash(plain, {
type: argon2.argon2id,
memoryCost: 19456, // KiB
timeCost: 2,
parallelism: 1,
}); // random per-user salt is generated and embedded
const ok = await argon2.verify(user.password_hash, plain);Tune the parameters to your hardware against current OWASP guidance; the salt is generated and stored inside the encoded hash.
bcrypt with a work factor
Vulnerable
import hashlib
stored = hashlib.sha1(plain.encode()).hexdigest()Hardened
import bcrypt
stored = bcrypt.hashpw(plain.encode(), bcrypt.gensalt(rounds=12))
ok = bcrypt.checkpw(candidate.encode(), stored)bcrypt truncates input at 72 bytes; prefer Argon2id for new systems.
- Store only the output of Argon2id, scrypt or bcrypt with a unique per-user salt; ban fast hashes on password data in coding standards.
- Rehash to the new scheme transparently at next successful login and flag legacy hashes for forced reset.
- Set a minimum length of at least 8 (prefer 12 or more), allow long passphrases, and remove composition and scheduled rotation rules.
- Screen new and changed passwords against a breached list using a k-anonymity API.
- Throttle login attempts per account and source, and require MFA for administrators and sensitive roles.
- Never log passwords or reset tokens, and use single-use, short-lived reset links delivered to a verified channel.
If it already happened
If a table may have leaked, force a reset for all affected accounts, invalidate sessions and tokens, and throttle the login endpoint while the fix ships.
Replace the hashing code with Argon2id or bcrypt, migrate legacy hashes at next login, remove passwords from logs and fix the reset flow.
Confirm users are on the new scheme, enable breached-password screening and MFA, and notify affected parties as regulations require.
Add a static-analysis rule against fast hashes on password data, a review checklist item, and monitoring for failed-login bursts.
Check yourself
1. Why is a fast, unsalted hash such as MD5 or SHA-256 a poor way to store passwords?
2. Which storage approach does current guidance recommend for passwords?
3. Which password policy change aligns with NIST SP 800-63B?