Skip to main content

Overview

This vulnerability occurs when an application uses a password hashing algorithm that is too fast, even if it’s cryptographically strong for other purposes (like SHA-256 or SHA-512). Fast hashes allow attackers to perform rapid offline brute-force or dictionary attacks if they obtain a database of password hashes. Modern password hashing requires algorithms designed to be computationally expensive (slow) and memory-hard to significantly hinder attackers.

Business Impact

If an attacker steals password hashes stored using fast algorithms, they can quickly crack many of them, especially common or weak passwords. This leads to widespread account compromise, allowing attackers to impersonate users, steal sensitive data linked to those accounts, and potentially pivot to other systems. 🔑💥

Reference Details

CWE ID: CWE-916 OWASP Top 10 (2021): A02:2021 - Cryptographic Failures Severity: High

Framework-Specific Analysis and Remediation

Secure password hashing relies on algorithms like bcrypt, scrypt, Argon2, or PBKDF2. The vulnerability arises when developers manually implement hashing using fast algorithms like SHA-256 directly, often combined with a salt but without sufficient iterations or work factor. Framework defaults are usually secure, but custom implementations or legacy code are common sources of this weakness.
  • Python
  • Java
  • .NET(C#)
  • PHP
  • Node.js
  • Ruby

Framework Context

Django defaults to PBKDF2_SHA256, which is acceptable but slower is better. Using hashlib.sha256 directly is the vulnerability.

Vulnerable Scenario 1: Direct SHA-256 Hashing

A custom user model uses hashlib.sha256 directly with just a salt.
# models.py (Custom User Model)
import hashlib
import os

def set_password(self, raw_password):
    self.salt = os.urandom(16)
    # DANGEROUS: SHA-256 is too fast for passwords, even with a salt.
    # Attackers can compute billions of hashes per second.
    self.password_hash = hashlib.sha256(self.salt + raw_password.encode()).hexdigest()

def check_password(self, raw_password):
    # Assuming salt is stored separately on the self object
    check_hash = hashlib.sha256(self.salt + raw_password.encode()).hexdigest()
    return check_hash == self.password_hash

Vulnerable Scenario 2: Using PBKDF2 with Low Iterations

Using hashlib.pbkdf2_hmac but setting the iteration count too low.
# utils/auth.py
import hashlib
import os

def hash_password_weak_pbkdf2(password):
    salt = os.urandom(16)
    # DANGEROUS: iterations=1000 is far too low for modern hardware.
    # OWASP recommends at least 600,000 for PBKDF2-SHA256 as of late 2023.
    # Check current OWASP recommendations.
    pw_hash = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 1000)
    # Store salt with hash, format: salt_hex$hash_hex
    return salt.hex() + '$' + pw_hash.hex()

Mitigation and Best Practices

Use Django’s built-in password management (user.set_password(raw_password), user.check_password(raw_password)). Configure PASSWORD_HASHERS in settings.py to prioritize Argon2PasswordHasher or BCryptSHA256PasswordHasher. Ensure PBKDF2 iterations are high if used.

Secure Code Example

# settings.py (Secure Password Hashers)
PASSWORD_HASHERS = [
    'django.contrib.auth.hashers.Argon2PasswordHasher', # Preferred
    'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
    'django.contrib.auth.hashers.PBKDF2PasswordHasher', # Default, ensure high iterations via Django version
]

# Usage with Django User model (Secure)
from django.contrib.auth.models import User # Assuming standard User model

# Assuming 'user' is a retrieved User instance
# user = User.objects.get(...)
user.set_password('newS3cureP@ssw0rd') # Uses hasher from settings
user.save()

is_correct = user.check_password('attempted_password')

Testing Strategy

Inspect password hashes in the database. They should start with argon2$, bcrypt$, or pbkdf2_sha256$. Manually check the iteration count if using PBKDF2 (it’s part of the stored hash string). Ensure no code uses hashlib.sha256 etc. directly for passwords.