Choose your stack
Fix weak password hashing in Node.js and Express
Hash with Argon2id at or above OWASP's minimum: argon2.hash(password, { type: argon2.argon2id, memoryCost: 19456, timeCost: 2, parallelism: 1 }).
Open the fix card Python · FlaskFix weak password hashing in Python and Flask
Use argon2-cffi's PasswordHasher with explicit parameters at or above OWASP's minimum, PasswordHasher(time_cost=2, memory_cost=19456, parallelism=1), and store ph.hash(password).
Open the fix card Java · Spring BootFix weak password hashing in Java and Spring Boot
Use Spring Security's DelegatingPasswordEncoder with an Argon2PasswordEncoder built from explicit parameters, new Argon2PasswordEncoder(16, 32, 1, 19456, 2), because defaultsForSpringSecurity_v5_8() is below OWASP's minimum.
Open the fix card C# · ASP.NET CoreFix weak password hashing in C# and ASP.NET Core
Use ASP.NET Core Identity's PasswordHasher<TUser> (PBKDF2-HMAC-SHA512, 128-bit salt) with PasswordHasherOptions.IterationCount = 220_000; .NET 10's default of 100,000 is below OWASP's figure.
Open the fix card Python · FastAPIFix weak password hashing in Python and FastAPI
Use argon2-cffi's PasswordHasher with explicit parameters at or above OWASP's minimum, PasswordHasher(time_cost=2, memory_cost=19456, parallelism=1), and store ph.hash(password) from a plain def path operation.
Open the fix card Python · DjangoFix weak password hashing in Python and Django
Install argon2-cffi and list django.contrib.auth.hashers.Argon2PasswordHasher first in PASSWORD_HASHERS, keeping older hashers after it.
Open the fix card Go · net/httpFix weak password hashing in Go and net/http
Hash new passwords with argon2.IDKey from golang.org/x/crypto at or above OWASP's minimum (memory 19456 KiB, 2 passes, 1 lane) with a 16-byte salt from crypto/rand, and store a PHC string with the parameters.
Open the fix card PHP · LaravelFix weak password hashing in PHP and Laravel
Set HASH_DRIVER=argon2id and use Laravel's Hash facade: Hash::make() writes a salted Argon2id hash that records its parameters, Hash::check() verifies it in constant time, and Hash::needsRehash() after a correct password tells you to store a new hash.
Open the fix cardStacks without a card
- TypeScript and Next.js: no card yet. The nearest is Fix weak password hashing in Node.js and Express, in the same language.
- Ruby on Rails: no card yet. The nearest is Fix weak password hashing in Python and Django.
How it happens
- The code stores sha256(password), or MD5 or SHA-1, as many older systems do. These hashes are built to be fast, and once an attacker has the table (through SQL injection, a leaked backup or an exposed replica) they guess offline, where rate limits, lockouts and MFA do not apply.
- Without a salt, equal passwords produce equal hashes, so one entry in a precomputed or shared lookup table cracks every account that chose that password. In this card's seed data Alice and Bob have the same stored value.
- A random salt per user is the usual half-fix. It defeats precomputed tables and hides shared passwords, but each guess still costs one SHA-256, so a dictionary attack against each row still runs at GPU speed.
- Slow hashes age. A hash keeps the parameters it was made with until something rehashes it, and that is only possible when the plaintext is available, at login. Library defaults are not always enough: Spring Security's Argon2 default is 16 MiB with 2 passes, ASP.NET Core Identity's is 100,000 PBKDF2 iterations and Werkzeug's scrypt default is N=2^15 with p=1, all below OWASP's minimums.
- Hand-written upgrade code adds its own bugs. Rehashing before the password is verified lets anyone who knows a username set that user's password, an upgrade that ignores a concurrent password change silently undoes it, and comparing hashes with == leaks through timing how much of the hash matched.
Fix principles
- Hash new passwords with a password-hashing function: Argon2id with at least m=19456 KiB, t=2, p=1, or one of OWASP's equivalent pairs (46 MiB with t=1, 12 MiB with t=3, 9 MiB with t=4, 7 MiB with t=5). Where only PBKDF2 is available, or FIPS-140 validation is required, use at least 600,000 iterations of HMAC-SHA256 or 220,000 of HMAC-SHA512.
- Let the library generate a random 16-byte salt for each hash, and store the algorithm, parameters, salt and hash together, as in the PHC string $argon2id$v=19$m=19456,t=2,p=1$<salt>$<hash> or Identity's versioned header. The policy can then change while old hashes still verify; NIST SP 800-63B-4 asks for this record of the scheme and cost factor.
- Upgrade only on success: after a password verifies, rehash it if the stored hash is legacy or below the current policy, and write it back with a compare-and-swap (UPDATE ... WHERE password_hash = <the hash you verified>) so that a password change made in the meantime wins.
- Deal with users who never log in: expire their legacy hashes after a deadline and require a reset, or wrap them now as Argon2id(SHA-256(password)) behind a marker (OWASP's second upgrade method). Wrapping takes them off lookup tables but leaves them easier to crack than a direct hash, so replace those at the next login too.
- Compare digests in constant time. The password-hashing libraries already do; a hand-written legacy check must use crypto.timingSafeEqual, hmac.compare_digest, MessageDigest.isEqual or CryptographicOperations.FixedTimeEquals.
- Accept long passwords and hash every byte: NIST SP 800-63B-4 asks verifiers to permit at least 64 characters and never to truncate. bcrypt reads only the first 72 bytes (64 CJK characters take 192), so it needs an explicit byte limit; Argon2 and PBKDF2 have none. Cap the length (these cards allow 256 characters) only to keep huge inputs away from the hash function.
- Budget the cost and keep the other defences: each concurrent Argon2 hash holds its memory (19 MiB at the minimum), so bound concurrency, and keep rate limiting, breached-password checks and MFA, because hashing protects stolen hashes, not the login endpoint.
How the cards prove it
- Run each card's tests in three modes: the fixed implementation must pass, and the vulnerable and the plausible-but-wrong implementations must fail. run-all.mjs does this for every card.
- The exploit tests read the users table directly, as an attacker with a copy would. New and upgraded hashes must parse as Argon2id (PBKDF2-HMAC-SHA512 in the C# card) at or above the OWASP minimum with a salt of at least 16 bytes; two users with the same password must get different hashes; a legacy SHA-256 row must be replaced after a correct login and left untouched after a wrong one; and a hash made with weaker parameters must be rehashed. In Node and Python a spy also checks that the legacy comparison uses the constant-time primitive.
- The functional tests keep legacy accounts working, reject a long password that differs from the real one only after byte 72, accept passwords with accents, emoji and CJK characters, and turn away a password over 256 characters with 400.
- Scanners are little help here. The Semgrep and Bandit password-hash rules look for MD5 or SHA-1, the .NET analyzers flag MD5 but not SHA-256, and Find Security Bugs flagged only the timing-unsafe String.equals. CodeQL has password-hashing queries for JavaScript and Python (read, not executed) but only experimental salt checks for Java and C#. Each card's scannerRules records what was run and what it found.
References
- OWASP Password Storage Cheat Sheet
- RFC 9106: Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications
- NIST SP 800-63B-4: Password verifiers
- OWASP ASVS 5.0.0: V11 Cryptography (11.4.2, password storage)
- OWASP ASVS 5.0.0: Appendix C, password hashing parameters
- OWASP Top 10:2025 A04 Cryptographic Failures
- CWE-916: Use of Password Hash With Insufficient Computational Effort
- CWE-759: Use of a One-Way Hash without a Salt
- CWE-327: Use of a Broken or Risky Cryptographic Algorithm
- CAPEC-55: Rainbow Table Password Cracking
- CAPEC-49: Password Brute Forcing
- C2SP: PHC string format (Argon2 parameter order)
- hashcat v7.0.0 benchmark on an RTX 4090 (SHA2-256: 22,138.4 MH/s)
Attack patterns:
Practice in the browser
- Password hashing lab: Verify and upgrade an Argon2 password hash on login · Practitioner, Python
Guides on this weakness
ECB mode: why it leaks patterns and what to use instead
Derive exactly what ECB reveals and why a longer key cannot fix it, find where ECB enters real code, and replace it with an authenticated mode.
Padding oracle attacks
Use a byte-level CBC example to see how a tiny validity signal becomes plaintext information, then design verification that checks more than the error message.
Hash collisions and digital signatures: MD5, SHA-1 and what to use
Distinguish collisions from preimages, follow a colliding pair through signing and verification, and retire MD5 and SHA-1 from the verifiers that still accept them.
RC4 keystream bias
Understand why fresh RC4 keys do not remove keystream bias and how repeated secret positions turn a small statistical effect into evidence.
64-bit block ciphers and the Sweet32 mechanism
Calculate the birthday effect for 64-bit blocks and derive the CBC relation that can turn a collision into a plaintext disclosure.
WEP: RC4 key reuse and weak IVs
Separate WEP’s repeated-keystream failure, weak-IV key recovery, and unkeyed integrity weakness through a concrete two-packet exercise.
Flame: MD5 certificate forgery
Follow how a weak digest and an overpowered certificate trust path enabled forged code-signing authority, without equating the event with RSA key theft.
Lucky Thirteen and POODLE: two different oracles
Compare two CBC oracles by their signal, protocol rules, and attacker prerequisites instead of treating them as interchangeable padding bugs.
DROWN: one RSA key across incompatible protocols
Map an RSA key across services to see how an SSLv2 oracle could compromise TLS traffic on a different endpoint.
Sweet32: the cost of a small block size
Read the 2016 Sweet32 demonstrations as evidence about traffic volume, repeated secrets, and session lifetime rather than treating a block-size threshold as an instant exploit.
Password storage: hashing parameters and rehash on login
Choose Argon2id, scrypt, PBKDF2 or bcrypt with current minimum parameters, store a versioned hash, and upgrade old hashes after a successful login without overwriting a reset.
AEAD records: AES-GCM nonces, associated data and key limits
Build an encrypted record whose tag binds ciphertext to the intended tenant, record, purpose and format, keep nonces unique within each key’s usage limit, and verify the design with actual AES-GCM.
Post-quantum migration: cryptographic inventory, hybrid TLS and deadlines
Inventory public-key cryptography by operation, owner and lifetime, turn on hybrid ML-KEM key exchange where it is already available, and plan signature and archive migration against the NIST, NSA, NCSC and EU dates.
Choose cryptographic algorithms and parameters by job
A sourced quick reference for October 2026: which algorithm and parameters to use for encryption, hashing, passwords, MACs, signatures, key agreement, key derivation, randomness and TLS, the matching APIs in Node.js, Python, Java, Go and .NET, and where post-quantum migration stands.
Case study: the LastPass breach (2022)
How an attacker went from a compromised developer laptop to a DevOps engineer's home computer, the keys to LastPass's cloud backups and a copy of customer vaults; why each vault's safety then rested on its master password and PBKDF2 iteration count; and what it teaches about backup keys, privileged endpoints, KDF upgrades and unencrypted metadata.