ASVS 5.0 V11.4.2 (Level 2) requires that passwords are stored using an approved, computationally intensive key derivation function, also called a password hashing function, with parameters set from current guidance. The settings should balance security and performance so that brute force stays expensive.
Why it matters
Database leaks happen. When one does, the only thing between your users' passwords and an attacker is how slow each guess is. A fast hash such as SHA-256 lets a GPU test billions of guesses per second. A well configured Argon2id lets the same hardware try thousands, and uses memory that makes parallel attacks costly.
How to implement it
Pick one of these, in order of preference:
- Argon2id, with memory, iterations and parallelism parameters.
- scrypt.
- bcrypt, noting its 72 byte input limit.
- PBKDF2 with HMAC SHA-256 or SHA-512 and a high iteration count, mainly where a FIPS requirement forces it.
Use the library's high level API, which handles salts and encoding for you.
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher(time_cost=3, memory_cost=64 * 1024, parallelism=4)
def store(password: str) -> str:
return ph.hash(password) # random salt, parameters embedded in the string
def check(stored: str, attempt: str) -> bool:
try:
ph.verify(stored, attempt)
except VerifyMismatchError:
return False
if ph.check_needs_rehash(stored):
pass # on success, rehash with current parameters and save
return True
Treat the parameters above as a starting point. Measure on your production hardware and tune so one hash takes a noticeable fraction of a second without hurting login capacity. Re read the OWASP Password Storage Cheat Sheet when you plan reviews, because recommended numbers move.
Upgrade path matters. Parameters live inside the stored string, so when check_needs_rehash says yes, rehash on the next successful login. For legacy fast hashes, wrap them: compute the new KDF over the old hash value in a migration, then rehash with the plain password at next login.
Consider a server side pepper stored in a KMS and mixed in with HMAC before hashing. It is optional, and it adds an operational burden, but it makes stolen database copies far less useful on their own.
Avoid denial of service. Limit concurrent hashing, cap input length at a sane value such as 1024 bytes before hashing, and rate limit login attempts.
How to test it
Read the user table. Hash strings should start with a recognisable prefix such as $argon2id$, and each must differ even for users with the same password, which proves per user salts. Time the verify function under load. Search the code for sha256(password, md5(password, or unsalted hashing calls. Then plant a test user with an old format hash and confirm login upgrades it.
Common mistakes
- Using a fast hash, with or without salt
- A global salt shared by all users
- Parameters left at library defaults without any measurement
- Truncating passwords silently with bcrypt
- No rehash on login, so weak records live forever
- Logging password attempts
Quick check
Why is SHA-256 alone wrong for passwords?
It is fast, so attackers can test enormous numbers of guesses per second after a leak.
How do you raise parameters later?
Store them with each hash, then rehash on the next successful login.
Sources
- OWASP ASVS
- OWASP Password Storage Cheat Sheet