General-purpose hashes used for passwords

General-purpose hashes used for passwords

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, and parallelism=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.
  • 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.

References