Use of a One-Way Hash with a Predictable Salt
Description
Use of a One-Way Hash with a Predictable Salt is a cryptographic vulnerability where software uses a one-way cryptographic hash for data that should remain irreversible (such as passwords), but implements a predictable salt during the hashing process. A salt is supposed to be a random value unique to each hash operation that prevents precomputation attacks. When the salt is predictable—such as using the username, a constant value, or a value derived from user-controllable data—attackers can precompute hashes for common passwords combined with known salts, defeating the purpose of salting.
Risk
Predictable salts significantly weaken password security. Attackers can build targeted rainbow tables using known or predictable salt values. If the salt is constant across all users, a single rainbow table attack compromises all passwords. If the salt is the username, attackers can precompute tables for common usernames. Even with per-user salts, if the salt pattern is predictable (like sequential numbers), attackers can compute targeted tables. While salts do prevent generic rainbow table attacks, predictable salts remain vulnerable to targeted dictionary attacks, especially with modern GPU-accelerated hash cracking. The weakness is amplified when combined with fast hash functions like MD5 or SHA-1.
Solution
Use cryptographically secure random number generators to create unique salts for each password. Store the salt alongside the hash. Use adaptive password hashing functions like bcrypt, scrypt, Argon2, or PBKDF2 that incorporate salting and are designed to be computationally expensive. These functions make both rainbow table attacks and brute-force attacks impractical. Configure the work factor high enough to make attacks costly while still being acceptable for legitimate authentication. Regularly increase the work factor as computing power increases.
Common Consequences
| Impact | Details |
|---|---|
| Access Control | Scope: Access Control Bypass Protection Mechanism - Predictable salts enable precomputation attacks that can recover passwords. |
| Confidentiality | Scope: Confidentiality Read Application Data - Recovered passwords may provide access to user accounts and sensitive data. |
Example Code
Vulnerable Code
# Vulnerable: Using username as salt
import hashlib
def vulnerable_hash_password(username, password):
# Vulnerable: Username is predictable and known to attackers
salt = username
# Attacker can precompute hashes for common usernames
hash_input = salt + password
password_hash = hashlib.sha256(hash_input.encode()).hexdigest()
return password_hash
# Vulnerable: Using constant salt
CONSTANT_SALT = "MySuperSecretSalt123" # Same for all users!
def vulnerable_constant_salt(password):
# Vulnerable: One rainbow table attacks ALL passwords
hash_input = CONSTANT_SALT + password
return hashlib.sha256(hash_input.encode()).hexdigest()
# Vulnerable: Using predictable derived salt
def vulnerable_derived_salt(user_id, password):
# Vulnerable: Salt derived from sequential user ID
salt = f"salt_{user_id}" # user_id is 1, 2, 3...
# Attackers can enumerate all possible salts
return hashlib.sha256((salt + password).encode()).hexdigest()
<?php
// Vulnerable: Hardcoded salt (CVE-2008-4905 pattern)
define('PASSWORD_SALT', 'blog_secret_2008');
function vulnerable_hash($password) {
// Vulnerable: Same salt for all passwords
return md5(PASSWORD_SALT . $password);
}
// Vulnerable: Using email domain as salt
function vulnerable_email_salt($email, $password) {
// Vulnerable: Domain is predictable
$domain = explode('@', $email)[1]; // e.g., "gmail.com"
// Attacker can build tables for common domains
return hash('sha256', $domain . $password);
}
// Vulnerable: Sequential salt (CVE-2002-1657 pattern)
function vulnerable_sequential_salt($user_id, $password) {
// Vulnerable: Predictable sequence
$salt = $user_id; // 1, 2, 3, 4...
return hash('sha256', $salt . $password);
}
?>
// Vulnerable: Java with predictable salt
import java.security.MessageDigest;
import java.nio.charset.StandardCharsets;
public class VulnerableHashing {
// Vulnerable: Static constant salt
private static final String SALT = "StaticSalt2024";
public String vulnerableHash(String password) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
// Vulnerable: Same salt for everyone
String salted = SALT + password;
byte[] hash = md.digest(salted.getBytes(StandardCharsets.UTF_8));
return bytesToHex(hash);
} catch (Exception e) {
return null;
}
}
// Vulnerable: Salt based on user attribute
public String vulnerableUserSalt(User user, String password) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
// Vulnerable: Username is predictable
String salted = user.getUsername() + password;
byte[] hash = md.digest(salted.getBytes(StandardCharsets.UTF_8));
return bytesToHex(hash);
} catch (Exception e) {
return null;
}
}
}
// Vulnerable: C with constant salt (CVE-2001-0967 pattern)
#include <string.h>
#include <openssl/sha.h>
#define CONSTANT_SALT "ServerSalt"
void vulnerable_hash(const char* password, unsigned char* output) {
char buffer[256];
// Vulnerable: Constant salt
snprintf(buffer, sizeof(buffer), "%s%s", CONSTANT_SALT, password);
SHA256((unsigned char*)buffer, strlen(buffer), output);
}
// Vulnerable: Salt from timestamp with low resolution
void vulnerable_time_salt(const char* password, unsigned char* output) {
char buffer[256];
time_t now = time(NULL);
// Vulnerable: Only ~31M possible values per year
// Attacker can enumerate timestamps around account creation
snprintf(buffer, sizeof(buffer), "%ld%s", now, password);
SHA256((unsigned char*)buffer, strlen(buffer), output);
}
Fixed Code
# Fixed: Use proper password hashing with random salt
import bcrypt
import argon2
import secrets
# Fixed: Using bcrypt (recommended)
def secure_hash_bcrypt(password):
# bcrypt generates random salt internally
# Work factor adjustable for security/performance balance
salt = bcrypt.gensalt(rounds=12)
password_hash = bcrypt.hashpw(password.encode(), salt)
return password_hash
def verify_bcrypt(password, stored_hash):
return bcrypt.checkpw(password.encode(), stored_hash)
# Fixed: Using Argon2 (current recommendation)
def secure_hash_argon2(password):
ph = argon2.PasswordHasher(
time_cost=3, # Number of iterations
memory_cost=65536, # 64MB memory usage
parallelism=4 # Parallel threads
)
return ph.hash(password)
def verify_argon2(password, stored_hash):
ph = argon2.PasswordHasher()
try:
return ph.verify(stored_hash, password)
except argon2.exceptions.VerifyMismatchError:
return False
# Fixed: Manual approach with cryptographic random salt
import hashlib
def secure_hash_manual(password):
# Fixed: Generate cryptographically random salt
salt = secrets.token_bytes(32) # 256 bits of randomness
# Use slow hash function
# Note: Still better to use bcrypt/argon2
hash_value = hashlib.pbkdf2_hmac(
'sha256',
password.encode(),
salt,
iterations=100000 # High iteration count
)
# Return salt + hash (salt needed for verification)
return salt + hash_value
<?php
// Fixed: PHP password hashing (PHP 5.5+)
function secure_hash($password) {
// Fixed: password_hash generates random salt automatically
// Uses bcrypt by default (PASSWORD_DEFAULT)
return password_hash($password, PASSWORD_DEFAULT, ['cost' => 12]);
}
function secure_verify($password, $stored_hash) {
return password_verify($password, $stored_hash);
}
// Fixed: Using Argon2 (PHP 7.2+)
function secure_hash_argon2($password) {
return password_hash($password, PASSWORD_ARGON2ID, [
'memory_cost' => 65536,
'time_cost' => 4,
'threads' => 3
]);
}
// Fixed: Manual approach with proper salt
function secure_manual_hash($password) {
// Fixed: Cryptographically random salt
$salt = random_bytes(32);
// Use PBKDF2 with high iterations
$hash = hash_pbkdf2('sha256', $password, $salt, 100000, 32, true);
// Return encoded salt and hash
return base64_encode($salt) . '$' . base64_encode($hash);
}
?>
// Fixed: Java with proper password hashing
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.SecureRandom;
public class SecureHashing {
// Fixed: Using BCrypt
private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);
public String secureBcryptHash(String password) {
// BCrypt generates random salt internally
return encoder.encode(password);
}
public boolean secureBcryptVerify(String password, String hash) {
return encoder.matches(password, hash);
}
// Fixed: Using PBKDF2 with random salt
public String securePbkdf2Hash(String password) throws Exception {
// Fixed: Generate random salt
SecureRandom random = new SecureRandom();
byte[] salt = new byte[32];
random.nextBytes(salt);
// High iteration count
int iterations = 100000;
PBEKeySpec spec = new PBEKeySpec(
password.toCharArray(), salt, iterations, 256);
SecretKeyFactory factory =
SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] hash = factory.generateSecret(spec).getEncoded();
// Return iterations:salt:hash
return iterations + ":" + bytesToHex(salt) + ":" + bytesToHex(hash);
}
}
// Fixed: C with proper salt generation
#include <string.h>
#include <openssl/rand.h>
#include <openssl/evp.h>
#define SALT_LENGTH 32
#define HASH_LENGTH 32
#define ITERATIONS 100000
int secure_hash(const char* password,
unsigned char* salt_out,
unsigned char* hash_out) {
// Fixed: Generate cryptographically random salt
if (RAND_bytes(salt_out, SALT_LENGTH) != 1) {
return -1; // RNG failure
}
// Use PBKDF2 with high iterations
if (PKCS5_PBKDF2_HMAC(password, strlen(password),
salt_out, SALT_LENGTH,
ITERATIONS,
EVP_sha256(),
HASH_LENGTH, hash_out) != 1) {
return -1;
}
return 0;
}
int secure_verify(const char* password,
const unsigned char* stored_salt,
const unsigned char* stored_hash) {
unsigned char computed_hash[HASH_LENGTH];
// Recompute hash with stored salt
if (PKCS5_PBKDF2_HMAC(password, strlen(password),
stored_salt, SALT_LENGTH,
ITERATIONS,
EVP_sha256(),
HASH_LENGTH, computed_hash) != 1) {
return 0;
}
// Constant-time comparison
return CRYPTO_memcmp(computed_hash, stored_hash, HASH_LENGTH) == 0;
}
CVE Examples
- CVE-2008-4905: Blogging software used hardcoded salt for password hashing.
- CVE-2002-1657: Database server used username as salt, enabling targeted brute force attacks.
- CVE-2001-0967: Server used constant salt for all passwords.
- CVE-2005-0408: Predictable MD5 hashes using constant values plus username allowed authentication bypass.
References
- MITRE Corporation. "CWE-760: Use of a One-Way Hash with a Predictable Salt." https://cwe.mitre.org/data/definitions/760.html
- OWASP. "Password Storage Cheat Sheet."
- NIST SP 800-132. "Recommendation for Password-Based Key Derivation."