Description
Applying a general-purpose hash such as SHA-2/3 or BLAKE2 directly to a password has a low computational cost, enabling fast guessing with GPUs or ASICs. An attacker with leaked hashes can test many candidates using dictionaries or brute force; weak or reused passwords are especially at risk. Exposure through a database, backup, or log may therefore lead to offline cracking and account takeover.
Potential impact
- Account takeover: Recovered passwords can allow an attacker to sign in.
- Compromise across services: Reused passwords may give access to other services.
- Wider data exposure: An administrator account may provide access to more systems and data.
- Unmet security requirements: Failure to meet the organization's applicable password-storage requirements may lead to audit findings.
Remediation
- Use a dedicated, adjustable-cost password hash such as Argon2id, scrypt, bcrypt, or PBKDF2.
- Configure an adequate work factor:
- Argon2id: the example below uses
time_cost=3, 64 MiB of memory, andparallelism=2. Benchmark and adjust for the production environment. - PBKDF2-HMAC-SHA256: use at least 600,000 iterations and tune the cost for server capacity.
- bcrypt: a work factor of 12 or higher is recommended here.
- scrypt: choose sufficiently high N/r/p values for the server's capacity.
- Argon2id: the example below uses
- Use a fresh random salt of at least 16 bytes for each user and store it with the hash. Do not reuse a salt.
- If using a pepper, keep the secret in protected storage separate from the database and use a vetted method to combine it.
- Use the password-hashing library's verification API. If direct digest comparison is necessary, use a constant-time comparison such as
hmac.compare_digest. - Also mitigate online attacks with adequate password length, a maximum input length, request rate limits, or account locking.
- Use dedicated libraries such as argon2-cffi or passlib instead of implementing password hashing yourself.
Examples
Before
python
import hashlib
# Incorrect: a fast general-purpose hash (sha3_256) and a static salt
STATIC_SALT = b"NaCl-2024" # Not a separate salt for each user
def insecure_hash_password(password: str) -> str:
data = password.encode("utf-8") + STATIC_SALT
digest = hashlib.sha3_256(data).hexdigest()
return digest
# Verification repeats the hash and compares strings (possible timing leakage)
def insecure_verify_password(password: str, stored_hash: str) -> bool:
calc = insecure_hash_password(password)
return calc == stored_hash
After
python
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
# Argon2id: memory-hard hashing increases the cost of GPU attacks
ph = PasswordHasher(time_cost=3, memory_cost=64 * 1024, parallelism=2, hash_len=32)
def secure_hash_password(password: str) -> str:
# The encoded hash includes an automatically generated random salt.
return ph.hash(password)
def secure_verify_password(password: str, stored_hash: str) -> bool:
try:
return ph.verify(stored_hash, password)
except VerifyMismatchError:
return False
Explanation:
- Before:
- Fast general hashes such as sha3_256 let GPUs or ASICs test candidates at low cost.
- A static salt produces the same hash for the same password across users and weakens protection against precomputed tables.
- Ordinary string comparison is not constant time and may leak timing information.
- After:
- Argon2id's memory requirements increase the cost of large-scale parallel guessing.
- PasswordHasher automatically includes a fresh random salt in each hash.
- Its verify method performs verification; benchmark the parameters and increase their cost as server capacity allows.