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. Usinghashlib.sha256 directly is the vulnerability.Vulnerable Scenario 1: Direct SHA-256 Hashing
A custom user model useshashlib.sha256 directly with just a salt.Copy
# 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
Usinghashlib.pbkdf2_hmac but setting the iteration count too low.Copy
# 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
Copy
# 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 withargon2$, 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.Framework Context
Spring Security defaults toBCryptPasswordEncoder, which is secure. The vulnerability is using MessageDigest (e.g., SHA-256) directly or a PasswordEncoder with insufficient work factor.Vulnerable Scenario 1: Direct SHA-256 Hashing
Manually hashing passwords usingjava.security.MessageDigest.Copy
// service/UserService.java
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64; // Added for encoding example
import java.nio.charset.StandardCharsets; // Added for encoding
public String hashPasswordSha256(String password) throws Exception {
SecureRandom random = new SecureRandom();
byte[] salt = new byte[16];
random.nextBytes(salt);
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(salt);
// DANGEROUS: SHA-256 is too fast, even with a salt.
byte[] hashedPassword = md.digest(password.getBytes(StandardCharsets.UTF_8));
// Combine salt and hash for storage (e.g., Base64 encoded)
return Base64.getEncoder().encodeToString(salt) + ":" + Base64.getEncoder().encodeToString(hashedPassword);
}
// Assume bytesToHex exists or use Base64 as above
// private static String bytesToHex(byte[] bytes) { /* ... */ return ""; }
Vulnerable Scenario 2: BCrypt with Low Strength
UsingBCryptPasswordEncoder but configuring it with a very low strength (work factor).Copy
// config/SecurityConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
// ... other imports ...
@Bean
public PasswordEncoder passwordEncoder() {
// DANGEROUS: Strength 4 is extremely weak and fast to compute.
// Default is 10, recommended is 12+.
int strength = 4;
return new BCryptPasswordEncoder(strength);
}
Mitigation and Best Practices
UseBCryptPasswordEncoder with a strength of at least 12, or use Argon2PasswordEncoder. Let Spring Security handle the hashing via PasswordEncoder.encode(rawPassword).Secure Code Example
Copy
// config/SecurityConfig.java (Secure)
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
// ... other imports ...
@Bean
public PasswordEncoder passwordEncoder() {
// SECURE: Use BCrypt with adequate strength (12 or higher).
// Or use Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
int strength = 12;
return new BCryptPasswordEncoder(strength);
}
// service/UserService.java (Secure Usage)
import org.springframework.beans.factory.annotation.Autowired; // Added import
import org.springframework.security.crypto.password.PasswordEncoder; // Added import
import org.springframework.stereotype.Service; // Added import
// ... other imports ...
@Service // Mark as a service for injection
public class UserService { // Added class definition
@Autowired // Add Autowired for injection
private PasswordEncoder passwordEncoder;
public String hashPasswordSecure(String rawPassword) {
// SECURE: Delegate hashing to the configured PasswordEncoder.
return passwordEncoder.encode(rawPassword);
}
// Assume method exists to check password:
// public boolean checkPassword(String rawPassword, String encodedPassword) {
// return passwordEncoder.matches(rawPassword, encodedPassword);
// }
}
Testing Strategy
Check password hashes in the database; they should start with$2a$12$ (BCrypt strength 12) or $argon2id$. Write a unit test for the PasswordEncoder bean to ensure it’s BCrypt or Argon2 with sufficient strength.Framework Context
ASP.NET Core Identity defaults toPBKDF2 with HMAC-SHA256 and a high iteration count (>= 10,000 in recent versions), which is acceptable. The vulnerability is using SHA256.Create() directly or configuring Identity with low iteration counts.Vulnerable Scenario 1: Direct SHA-256 Hashing
Manually hashing passwords usingSystem.Security.Cryptography.SHA256.Copy
// Services/AuthService.cs
using System.Security.Cryptography;
using System.Text;
using System; // Added for Convert, Buffer
public string HashPasswordSha256(string password, byte[] salt)
{
using (var sha256 = SHA256.Create())
{
byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
byte[] saltedPassword = new byte[salt.Length + passwordBytes.Length];
// Combine salt and password bytes
Buffer.BlockCopy(salt, 0, saltedPassword, 0, salt.Length);
Buffer.BlockCopy(passwordBytes, 0, saltedPassword, salt.Length, passwordBytes.Length);
// DANGEROUS: SHA-256 is too fast for passwords.
byte[] hashBytes = sha256.ComputeHash(saltedPassword);
return Convert.ToBase64String(salt) + ":" + Convert.ToBase64String(hashBytes);
}
}
Vulnerable Scenario 2: Low Iteration Count for PBKDF2
Configuring ASP.NET Core Identity’sPasswordHasher with insufficient iterations.Copy
// Startup.cs (ConfigureServices)
using Microsoft.AspNetCore.Identity; // Added namespace
using Microsoft.Extensions.DependencyInjection; // Added namespace for IServiceCollection
using Microsoft.EntityFrameworkCore; // Added for DbContext example
// ... other using statements ...
services.Configure<PasswordHasherOptions>(options =>
// DANGEROUS: Iteration count of 1000 is too low.
// Default is higher (e.g., 10000+ in .NET Core 3+, 100k+ in .NET 6+).
options.IterationCount = 1000
);
// Add Identity services after configuring options
services.AddDefaultIdentity<IdentityUser>().AddEntityFrameworkStores<ApplicationDbContext>(); // Example assuming IdentityUser, ApplicationDbContext
Mitigation and Best Practices
Use the default ASP.NET Core Identity password hasher (PasswordHasher<TUser>). Ensure the PasswordHasherOptions.IterationCount is set to a high value (OWASP recommends 600,000+ for PBKDF2-SHA256). Consider using BCrypt.Net or other libraries if stronger hashing is needed.Secure Code Example
Copy
// Startup.cs (ConfigureServices - Secure Default)
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
// ... other using statements ...
// Assuming ApplicationDbContext and IdentityUser exist
public void ConfigureServices(IServiceCollection services) // Example method signature
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); // Example DBContext
// SECURE: Rely on the default high iteration count (check your .NET version's default).
services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
// Optional: Explicitly configure for higher iterations if default isn't enough
// services.Configure<PasswordHasherOptions>(options => options.IterationCount = 600000);
}
// Usage (Secure - assuming UserManager is injected)
// In a Controller or Service:
// private readonly UserManager<IdentityUser> _userManager;
// public YourClass(UserManager<IdentityUser> userManager) { _userManager = userManager; }
//
// public async Task CreateUser(RegisterViewModel model) {
// var user = new IdentityUser { UserName = model.Email, Email = model.Email };
// // SECURE: UserManager uses the configured (secure) hasher.
// var result = await _userManager.CreateAsync(user, model.Password);
// }
Testing Strategy
Check thePasswordHash column in AspNetUsers. While the format encodes parameters, verifying the iteration count requires either attempting a hash verification (which uses the embedded count) or decoding the format. Ensure no code manually uses SHA256.Create().ComputeHash().Framework Context
Laravel defaults tobcrypt (Hash::make), which is secure. The vulnerability is using hash('sha256', ...) or md5() manually.Vulnerable Scenario 1: Using hash('sha256', ...)
A developer manually hashes a password using the generic hash() function.Copy
// app/Http/Controllers/RegisterController.php
use App\Models\User; // Added namespace
// ... other imports ...
protected function create(array $data)
{
$salt = random_bytes(16);
// DANGEROUS: 'sha256' is too fast, even with a salt.
$passwordHash = hash('sha256', $salt . $data['password']);
return User::create([
'name' => $data['name'],
'email' => $data['email'],
'salt' => base64_encode($salt), // Assuming salt is stored separately
'password' => $passwordHash,
]);
}
Vulnerable Scenario 2: Low bcrypt Cost Factor
Configuring Laravel’s bcrypt hasher with a low cost factor (rounds).Copy
// config/hashing.php
return [
'default' => 'bcrypt', // Ensure bcrypt is default or explicitly chosen
// ... other drivers ...
'bcrypt' => [
// DANGEROUS: 'rounds' => 4 is extremely weak.
// Default is 10 or 12 depending on Laravel version, recommended is 12+.
'rounds' => 4,
],
// ...
];
Mitigation and Best Practices
Always useHash::make() and Hash::check(). Ensure the bcrypt rounds in config/hashing.php is at least 12. Consider switching the default driver to argon2id if your server supports it.Secure Code Example
Copy
// config/hashing.php (Secure)
return [
'default' => env('HASH_DRIVER', 'bcrypt'), // Default driver
'bcrypt' => [
'rounds' => env('BCRYPT_ROUNDS', 12), // SECURE: Use a sufficient cost factor (12+)
],
'argon2id' => [
'memory' => 65536, // 64MB (adjust based on server resources)
'threads' => 1, // Adjust based on server cores
'time' => 4, // Adjust based on desired time cost
],
];
// app/Http/Controllers/RegisterController.php (Secure Usage)
use Illuminate\Support\Facades\Hash;
use App\Models\User; // Added namespace
// ... other imports ...
protected function create(array $data)
{
return User::create([
'name' => $data['name'],
'email' => $data['email'],
// SECURE: Uses Laravel's configured default (bcrypt or Argon2).
'password' => Hash::make($data['password']),
]);
}
Testing Strategy
Check password hashes in the database; they should start with$2y$12$ (bcrypt cost 12) or $argon2id$. Verify the rounds in config/hashing.php. Ensure no code uses hash(), md5(), or sha1() for passwords.Framework Context
Node’s built-incrypto.pbkdf2 is available but requires careful parameter selection. The standard is the bcrypt library. Using crypto.createHash('sha256', ...) is the vulnerability.Vulnerable Scenario 1: Direct SHA-256 Hashing
Hashing passwords using the built-incrypto module directly.Copy
// utils/auth.js
const crypto = require('crypto');
function hashPasswordSha256(password) {
const salt = crypto.randomBytes(16);
// DANGEROUS: SHA-256 is too fast.
const hash = crypto.createHash('sha256').update(salt.toString('hex') + password).digest('hex');
return salt.toString('hex') + ':' + hash; // Store salt separately
}
Vulnerable Scenario 2: Using crypto.pbkdf2 with Low Iterations
Using the built-in PBKDF2 but with insufficient iterations.Copy
// utils/auth.js
const crypto = require('crypto');
function hashPasswordWeakPbkdf2(password, callback) {
const salt = crypto.randomBytes(16);
// DANGEROUS: 1000 iterations is too low. Need 600k+ for sha512 now.
crypto.pbkdf2(password, salt, 1000, 64, 'sha512', (err, derivedKey) => {
if (err) return callback(err); // Pass error to callback
// Store salt with hash
callback(null, salt.toString('hex') + ':' + derivedKey.toString('hex'));
});
}
Mitigation and Best Practices
Use thebcrypt library (bcrypt.hash, bcrypt.compare). Ensure the salt rounds parameter is 12 or higher. Alternatively use Node’s crypto.pbkdf2 with a very high iteration count (check OWASP recommendations).Secure Code Example
Copy
// utils/auth.js (Secure with bcrypt)
const bcrypt = require('bcrypt');
const saltRounds = 12; // SECURE: Recommended salt rounds
async function hashPasswordSecure(password) {
// SECURE: Uses bcrypt async with salt rounds (salt generated automatically)
const hash = await bcrypt.hash(password, saltRounds);
return hash; // bcrypt hash includes salt and cost factor
}
async function comparePassword(password, hash) {
// Handles parsing salt/cost from hash automatically
return await bcrypt.compare(password, hash);
}
// utils/auth.js (Secure with crypto.pbkdf2)
const crypto = require('crypto');
const util = require('util'); // For promisify
const pbkdf2Async = util.promisify(crypto.pbkdf2);
async function hashPasswordSecurePbkdf2(password) {
const salt = crypto.randomBytes(16);
const iterations = 600000; // SECURE: Use high iteration count (check current OWASP)
const keylen = 64;
const digest = 'sha512';
const derivedKey = await pbkdf2Async(password, salt, iterations, keylen, digest);
// Store salt, iterations, digest, keylen with the hash
return `pbkdf2_${digest}$${iterations}$${salt.toString('hex')}$${derivedKey.toString('hex')}`;
}
Testing Strategy
Check password hashes in the database; they should start with$2b$12$. Ensure no code uses crypto.createHash directly for passwords. If using crypto.pbkdf2, verify the iteration count is high (e.g., > 600,000).Framework Context
Rails’has_secure_password uses BCrypt::Password.create, which is secure. The vulnerability is using Digest::SHA256.hexdigest manually.Vulnerable Scenario 1: Direct SHA-256 Hashing
Manually hashing a password usingDigest::SHA256.Copy
# app/models/legacy_user.rb
require 'digest'
require 'securerandom'
class LegacyUser < ApplicationRecord # Assume base class exists
# Example manual hashing (vulnerable)
def password=(new_password)
self.salt = SecureRandom.hex(16)
# DANGEROUS: SHA256 is too fast.
self.password_digest = Digest::SHA256.hexdigest(self.salt + new_password)
end
# Need a corresponding check_password method too
end
Vulnerable Scenario 2: Low Cost Factor for bcrypt
Manually usingBCrypt::Password.create with a low cost factor.Copy
# lib/custom_hasher.rb
require 'bcrypt'
module CustomHasher
def self.hash_password_weak_bcrypt(password)
# DANGEROUS: Cost factor 4 is extremely weak.
# Default is usually 12.
BCrypt::Password.create(password, cost: 4)
end
end
Mitigation and Best Practices
Usehas_secure_password in your Active Record model. This leverages the bcrypt gem, which uses a secure default cost factor. If hashing manually, use BCrypt::Password.create(password) which uses the default cost.Secure Code Example
Copy
# app/models/user.rb (Secure)
class User < ApplicationRecord
# SECURE: Uses BCrypt::Password with default cost factor (>= 12).
# Requires 'bcrypt' gem in Gemfile.
has_secure_password
end
# lib/custom_hasher.rb (Secure Manual Hashing)
require 'bcrypt'
module CustomHasher
def self.hash_password_secure(password)
# SECURE: Uses default cost factor (>= 12)
BCrypt::Password.create(password)
end
def self.verify_password(password, hash)
# Check if hash is valid BCrypt hash before attempting compare
return false unless BCrypt::Password.valid_hash?(hash)
bcrypt_hash = BCrypt::Password.new(hash)
# SECURE: Comparison is timing-attack resistant
bcrypt_hash == password # Compares password against embedded salt/hash
end
end
Testing Strategy
Check thepassword_digest column in your database; hashes should start with $2a$12$ (bcrypt cost 12). Ensure no code manually uses Digest::SHA256 etc. for passwords. If using BCrypt::Password.create manually, verify the cost factor isn’t explicitly set low.
