Skip to main content

Overview

This vulnerability occurs when an application hashes passwords using a one-way cryptographic hash function (like SHA-256) but fails to include a unique, random salt for each user. A salt is a random value added to the password before hashing. Without a salt, identical passwords will produce identical hashes. Attackers can precompute hashes for common passwords (a “rainbow table”) and quickly find matches if they obtain the hash database. 🌈💥

Business Impact

Hashing without a salt makes password cracking significantly faster and easier for attackers who steal the hash database. Even if a strong hashing algorithm is used, the lack of salt allows attackers to crack multiple identical passwords simultaneously using precomputed tables. This leads to mass account compromise.

Reference Details

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

Framework-Specific Analysis and Remediation

Modern password hashing functions provided by frameworks (bcrypt, Argon2, PBKDF2 implementations) automatically generate and manage unique salts for each password. This vulnerability primarily occurs when developers implement password hashing manually using basic hash functions (like SHA-256) and either forget the salt entirely or use a static, non-unique salt. The fix is to always use the framework’s recommended password hashing functions, which handle salting correctly.
  • Python
  • Java
  • .NET(C#)
  • PHP
  • Node.js
  • Ruby

Framework Context

Using hashlib.sha256(password.encode()).hexdigest() directly without adding a salt.

Vulnerable Scenario 1: Direct Hashing (No Salt)

A custom user model hashes the password directly without any salt.
# models.py (Custom User Model)
import hashlib

def set_password(self, raw_password):
    # DANGEROUS: No salt is used. Two users with the same password
    # will have the same hash, making rainbow tables effective.
    self.password_hash = hashlib.sha256(raw_password.encode()).hexdigest()
    
def check_password(self, raw_password):
    check_hash = hashlib.sha256(raw_password.encode()).hexdigest()
    return check_hash == self.password_hash

Vulnerable Scenario 2: Static Salt

A developer adds a salt, but it’s a hardcoded, static value used for all users.
# models.py (Custom User Model with Static Salt)
import hashlib

# DANGEROUS: Using a single, static salt is almost as bad as no salt.
# Precomputation is still possible for this specific salt.
STATIC_SALT = b"my_super_secret_static_salt" 

def set_password(self, raw_password):
    salted_input = STATIC_SALT + raw_password.encode()
    self.password_hash = hashlib.sha256(salted_input).hexdigest()
    
def check_password(self, raw_password):
    salted_input = STATIC_SALT + raw_password.encode()
    check_hash = hashlib.sha256(salted_input).hexdigest()
    return check_hash == self.password_hash

Mitigation and Best Practices

Use Django’s built-in password management (user.set_password(), user.check_password()) which automatically handles unique salting and uses strong algorithms (like PBKDF2, bcrypt, Argon2).

Secure Code Example

# models.py (Using Django's AbstractUser or similar)
from django.contrib.auth.models import User # Or AbstractUser

# SECURE: Django's built-in methods handle salting correctly.
user = User.objects.get(username='test')
user.set_password('newS3cureP@ssw0rd') 
user.save()

is_correct = user.check_password('attempted_password') 

Testing Strategy

Inspect password hashes in the database. Hashes generated by Django’s secure hashers (PBKDF2, bcrypt, Argon2) embed the salt within the hash string itself. You should see different hash values even for users with the same password. Ensure no code directly uses hashlib.shaXXX() without a unique, per-user salt.