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. 🌈💥
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-759OWASP Top 10 (2021): A02:2021 - Cryptographic Failures
Severity: High
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.
A custom user model hashes the password directly without any salt.
# models.py (Custom User Model)import hashlibdef 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
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
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.
Using java.security.MessageDigest directly without prepending a unique, per-user salt. Spring Security’s PasswordEncoder implementations (like BCryptPasswordEncoder) handle salting internally.
Using a single salt value loaded from configuration for all passwords.
// service/UserService.javaimport java.security.MessageDigest;// Injected from config or hardcodedprivate static final byte[] STATIC_SALT = "load_from_config".getBytes(); public String hashPasswordStaticSalt(String password) throws Exception { MessageDigest md = MessageDigest.getInstance("SHA-256"); // DANGEROUS: Using the same salt for every user. md.update(STATIC_SALT); byte[] hashedPassword = md.digest(password.getBytes("UTF-8")); return bytesToHex(hashedPassword); // Note: The salt is not even stored with the hash here!}
Use Spring Security’s PasswordEncoder interface with a secure implementation like BCryptPasswordEncoder or Argon2PasswordEncoder. These generate and embed a unique salt within the resulting hash string automatically.
Check password hashes in the database; bcrypt hashes start with $2a$ (or similar) and include the salt and cost factor. Ensure MessageDigest.getInstance("SHA-...") is not used directly for password hashing without proper, unique salting.
ASP.NET Core Identity’s PasswordHasher correctly generates and uses unique salts per password. The vulnerability occurs when manually hashing using SHA256.Create() without a salt.
Manually hashing passwords using System.Security.Cryptography.SHA256 without salt.
// Services/AuthService.csusing System.Security.Cryptography;using System.Text;public string HashPasswordSha256NoSalt(string password){ using (var sha256 = SHA256.Create()) { byte[] passwordBytes = Encoding.UTF8.GetBytes(password); // DANGEROUS: No salt is included in the hash input. byte[] hashBytes = sha256.ComputeHash(passwordBytes); return Convert.ToBase64String(hashBytes); // Salt is not stored either. }}
Use the built-in ASP.NET Core Identity services (UserManager<TUser>, SignInManager<TUser>). The PasswordHasher<TUser> automatically handles generation, storage (embedding the salt in the hash string), and verification.
// Using ASP.NET Core Identity (Secure)public class AccountController : Controller{ private readonly UserManager<IdentityUser> _userManager; public AccountController(UserManager<IdentityUser> userManager) { _userManager = userManager; } public async Task<IActionResult> Register(RegisterViewModel model) { var user = new IdentityUser { /* ... */ }; // SECURE: CreateAsync uses the configured PasswordHasher, // which handles salting correctly. var result = await _userManager.CreateAsync(user, model.Password); // ... } public async Task<bool> CheckPasswordSignIn(LoginViewModel model) { var user = await _userManager.FindByEmailAsync(model.Email); // SECURE: CheckPasswordAsync handles salt extraction and verification. return await _userManager.CheckPasswordAsync(user, model.Password); }}
Check the PasswordHash column in AspNetUsers. Hashes generated by Identity include salt and iteration count information, usually Base64 encoded. Verify that different users with the same password have different hashes. Ensure SHA256.Create().ComputeHash() is not used directly for passwords.
Laravel’s Hash::make() (using bcrypt or Argon2) handles salting automatically and securely. The vulnerability is using hash('sha256', ...) or md5() without a unique salt.
Vulnerable Scenario 2: Using crypt() with Weak Salt/Method
Using the older crypt() function with a weak method or a predictable/static salt.
// app/Utils/LegacyAuth.phpfunction hashPasswordCryptWeak($password) { // DANGEROUS: Using CRYPT_MD5 method (prefix $1$) is weak. // DANGEROUS: Using a static salt 'staticsalt' makes it easily breakable. return crypt($password, '$1$staticsalt$'); }
Always use Hash::make() and Hash::check(). These functions use bcrypt or Argon2id by default, which include robust, automatic salting. Avoid md5(), sha1(), hash(), and crypt() for password hashing.
Check password hashes in the database; they should start with $2y$ (bcrypt) or $argon2id$ (Argon2id). These formats include the salt. Verify that two users created with the same password have different hash strings. Ensure hash(), md5(), sha1(), crypt() are not used for password hashing.
Node’s built-in crypto module requires manual salt handling. The standard bcrypt library handles salting automatically. Using crypto.createHash(...) without a unique salt is the vulnerability.
// utils/auth.js (Secure with bcrypt)const bcrypt = require('bcrypt');const saltRounds = 12; async function hashPasswordSecure(password) { // SECURE: bcrypt.hash generates a unique salt automatically. const hash = await bcrypt.hash(password, saltRounds); // The returned hash string contains the salt, cost factor, and hash. return hash; }async function comparePassword(password, hash) { // SECURE: bcrypt.compare extracts the salt from the hash string. return await bcrypt.compare(password, hash);}
Check password hashes in the database; they should start with $2b$12$ (bcrypt, cost 12). Verify that users with the same password have different hash strings. Ensure crypto.createHash is not used directly for password hashing.
Rails’ has_secure_password uses BCrypt::Password.create, which handles salting automatically. The vulnerability is using Digest::SHA256.hexdigest without a unique salt.
Manually hashing a password using Digest::SHA256 without salt.
# app/models/legacy_user.rbrequire 'digest'def password=(new_password) # DANGEROUS: No salt is used. self.password_digest = Digest::SHA256.hexdigest(new_password)end
# app/models/legacy_user.rbrequire 'digest'# DANGEROUS: Single salt for all users.STATIC_SALT = "my-app-wide-secret-salt"def password=(new_password) salted_input = STATIC_SALT + new_password self.password_digest = Digest::SHA256.hexdigest(salted_input)end
Use has_secure_password in your Active Record model. This leverages the bcrypt gem, which generates and stores a unique salt per password automatically.
Check the password_digest column in your database; hashes should start with $2a$. Verify that different users with the same password have different hash strings. Ensure Digest::SHA256 or similar are not used directly for password hashing.