Safeguard
Application Security•

ASVS 5.0 V11.4.2: Store Passwords With Argon2id or Similar

ASVS V11.4.2 requires passwords stored with an approved slow password hashing function and sensible parameters. Argon2id, scrypt, bcrypt and PBKDF2 compared, with code.

Safeguard Security Team
3 min read

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:

  1. Argon2id, with memory, iterations and parallelism parameters.
  2. scrypt.
  3. bcrypt, noting its 72 byte input limit.
  4. 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

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.