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

ImpactDetails
Access ControlScope: Access Control

Bypass Protection Mechanism - Predictable salts enable precomputation attacks that can recover passwords.
ConfidentialityScope: 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

  1. MITRE Corporation. "CWE-760: Use of a One-Way Hash with a Predictable Salt." https://cwe.mitre.org/data/definitions/760.html
  2. OWASP. "Password Storage Cheat Sheet."
  3. NIST SP 800-132. "Recommendation for Password-Based Key Derivation."