Use of Password Hash With Insufficient Computational Effort

Description

Use of Password Hash With Insufficient Computational Effort occurs when software uses a password hashing algorithm that does not require significant computational resources to compute. Fast hash functions like MD5, SHA-1, or even SHA-256 without key stretching can be computed billions of times per second on modern GPUs. This allows attackers to perform brute-force attacks efficiently, testing vast numbers of password combinations in reasonable time.

Risk

Modern GPUs can compute billions of MD5 or SHA-256 hashes per second. A password database hashed with fast algorithms can be attacked offline without rate limiting. The 2012 LinkedIn breach showed how quickly unsalted SHA-1 hashes could be cracked. Even with salts, fast algorithms allow systematic brute-force attacks. Password cracking hardware and cloud services make these attacks accessible to anyone. Critical applications using weak hashing face inevitable credential compromise when breached.

Solution

Use password hashing algorithms specifically designed to be computationally expensive: bcrypt, scrypt, or Argon2. Configure work factors appropriately—the hash should take at least 100ms to compute. Increase work factors over time as hardware improves. Argon2 (winner of the Password Hashing Competition) is the current best practice. Use memory-hard functions that resist GPU/ASIC acceleration. Implement automatic rehashing when users authenticate to upgrade old hashes.

Common Consequences

ImpactDetails
ConfidentialityScope: Password Cracking

Fast hash functions allow billions of password guesses per second.
AuthenticationScope: Credential Compromise

Brute-forced passwords lead to account takeover.
ComplianceScope: Regulatory Violations

Many regulations require strong password storage (GDPR, PCI-DSS).

Example Code + Solution Code

Vulnerable Code

# VULNERABLE: Fast hash function (SHA-256)
import hashlib
import os

def hash_password_vulnerable(password):
    # SHA-256 is fast - billions per second on GPU!
    salt = os.urandom(16)
    hash_value = hashlib.sha256(salt + password.encode()).hexdigest()
    return f"{salt.hex()}:{hash_value}"

# VULNERABLE: Single iteration
def hash_password_single_iteration(password, salt):
    return hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 1)

# VULNERABLE: Low iteration count
def hash_password_low_iterations(password):
    salt = os.urandom(16)
    # 1000 iterations is far too low!
    key = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 1000)
    return f"{salt.hex()}:{key.hex()}"

# VULNERABLE: MD5 - extremely fast
def hash_password_md5(password):
    return hashlib.md5(password.encode()).hexdigest()
// VULNERABLE: Fast hash function
import java.security.MessageDigest;
import java.util.Base64;

public class VulnerablePasswordHash {

    // SHA-256 without key stretching - way too fast!
    public String hashPassword(String password, byte[] salt) throws Exception {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        md.update(salt);
        byte[] hash = md.digest(password.getBytes("UTF-8"));
        return Base64.getEncoder().encodeToString(hash);
    }

    // VULNERABLE: Low PBKDF2 iterations
    public String hashPasswordWeakPBKDF2(String password) throws Exception {
        byte[] salt = new byte[16];
        new SecureRandom().nextBytes(salt);

        // 1000 iterations is insufficient!
        PBEKeySpec spec = new PBEKeySpec(
            password.toCharArray(),
            salt,
            1000,  // TOO LOW!
            256
        );

        SecretKeyFactory factory =
            SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
        byte[] hash = factory.generateSecret(spec).getEncoded();

        return Base64.getEncoder().encodeToString(salt) + ":" +
               Base64.getEncoder().encodeToString(hash);
    }
}
// VULNERABLE: Fast hashing in Node.js
const crypto = require('crypto');

// SHA-256 is too fast for passwords
function hashPasswordVulnerable(password) {
    const salt = crypto.randomBytes(16);
    const hash = crypto.createHash('sha256')
        .update(salt)
        .update(password)
        .digest('hex');
    return `${salt.toString('hex')}:${hash}`;
}

// VULNERABLE: Low iterations
function hashPasswordLowIterations(password) {
    const salt = crypto.randomBytes(16);
    // 1000 iterations is far too low!
    const hash = crypto.pbkdf2Sync(password, salt, 1000, 64, 'sha256');
    return `${salt.toString('hex')}:${hash.toString('hex')}`;
}

// VULNERABLE: Using scrypt with low parameters
function hashPasswordWeakScrypt(password) {
    const salt = crypto.randomBytes(16);
    // N=1024 is too low!
    const hash = crypto.scryptSync(password, salt, 64, { N: 1024, r: 8, p: 1 });
    return `${salt.toString('hex')}:${hash.toString('hex')}`;
}

Fixed Code

# SAFE: Using bcrypt with appropriate work factor
import bcrypt

def hash_password_bcrypt(password):
    # Work factor 12 = 2^12 iterations
    # Adjust based on your performance requirements
    salt = bcrypt.gensalt(rounds=12)
    hashed = bcrypt.hashpw(password.encode(), salt)
    return hashed.decode()

def verify_password_bcrypt(password, stored_hash):
    return bcrypt.checkpw(password.encode(), stored_hash.encode())

# SAFE: Using Argon2 (recommended)
from argon2 import PasswordHasher, Type
from argon2.exceptions import VerifyMismatchError

# Argon2id is the recommended variant
ph = PasswordHasher(
    time_cost=3,          # Number of iterations
    memory_cost=65536,    # 64 MB memory
    parallelism=4,        # 4 parallel threads
    hash_len=32,          # Output length
    salt_len=16,          # Salt length
    type=Type.ID          # Argon2id
)

def hash_password_argon2(password):
    return ph.hash(password)

def verify_password_argon2(password, stored_hash):
    try:
        # Verify also checks if rehashing is needed
        ph.verify(stored_hash, password)

        # Check if parameters need updating
        if ph.check_needs_rehash(stored_hash):
            return True, hash_password_argon2(password)  # Return new hash

        return True, None
    except VerifyMismatchError:
        return False, None

# SAFE: PBKDF2 with high iteration count
import hashlib
import os

# OWASP 2023 recommendations for PBKDF2
PBKDF2_ITERATIONS = 600000  # For SHA-256
PBKDF2_ITERATIONS_SHA512 = 210000  # For SHA-512

def hash_password_pbkdf2(password):
    salt = os.urandom(32)  # 32 bytes = 256 bits

    key = hashlib.pbkdf2_hmac(
        'sha256',
        password.encode(),
        salt,
        PBKDF2_ITERATIONS,
        dklen=32
    )

    # Store iteration count for future-proofing
    return f"pbkdf2:sha256:{PBKDF2_ITERATIONS}:{salt.hex()}:{key.hex()}"

def verify_password_pbkdf2(password, stored_hash):
    parts = stored_hash.split(':')
    algorithm = parts[1]
    iterations = int(parts[2])
    salt = bytes.fromhex(parts[3])
    stored_key = bytes.fromhex(parts[4])

    computed_key = hashlib.pbkdf2_hmac(
        algorithm,
        password.encode(),
        salt,
        iterations,
        dklen=len(stored_key)
    )

    # Constant-time comparison
    return hmac.compare_digest(computed_key, stored_key)

# SAFE: scrypt with strong parameters
def hash_password_scrypt(password):
    salt = os.urandom(32)

    # Strong parameters for scrypt
    # n = CPU/memory cost (must be power of 2)
    # r = block size
    # p = parallelization factor
    key = hashlib.scrypt(
        password.encode(),
        salt=salt,
        n=2**17,    # 131072 - high cost
        r=8,
        p=1,
        dklen=32
    )

    return f"scrypt:131072:8:1:{salt.hex()}:{key.hex()}"
// SAFE: Java password hashing with bcrypt
import org.mindrot.jbcrypt.BCrypt;

public class SecurePasswordHash {

    // Work factor 12 is a good starting point
    // Increase over time as hardware improves
    private static final int WORK_FACTOR = 12;

    public String hashPassword(String password) {
        return BCrypt.hashpw(password, BCrypt.gensalt(WORK_FACTOR));
    }

    public boolean verifyPassword(String password, String storedHash) {
        return BCrypt.checkpw(password, storedHash);
    }

    // Check if rehashing is needed (work factor increased)
    public boolean needsRehash(String storedHash) {
        // Extract work factor from bcrypt hash
        String[] parts = storedHash.split("\\$");
        if (parts.length >= 4) {
            int currentWorkFactor = Integer.parseInt(parts[2]);
            return currentWorkFactor < WORK_FACTOR;
        }
        return true;
    }
}

// SAFE: Using Argon2 in Java
import de.mkammerer.argon2.Argon2;
import de.mkammerer.argon2.Argon2Factory;

public class Argon2PasswordHash {

    private final Argon2 argon2;

    public Argon2PasswordHash() {
        // Use Argon2id (recommended)
        this.argon2 = Argon2Factory.create(Argon2Factory.Argon2Types.ARGON2id);
    }

    public String hashPassword(String password) {
        // iterations, memory (KB), parallelism
        return argon2.hash(
            3,      // iterations
            65536,  // 64 MB memory
            4,      // parallelism
            password.toCharArray()
        );
    }

    public boolean verifyPassword(String password, String storedHash) {
        try {
            return argon2.verify(storedHash, password.toCharArray());
        } finally {
            // Clear password from memory
            argon2.wipeArray(password.toCharArray());
        }
    }

    public boolean needsRehash(String storedHash) {
        return argon2.needsRehash(storedHash, 3, 65536, 4);
    }
}

// SAFE: Strong PBKDF2 configuration
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.SecureRandom;

public class StrongPBKDF2 {

    // OWASP 2023 recommendation
    private static final int ITERATIONS = 600000;
    private static final int KEY_LENGTH = 256;
    private static final int SALT_LENGTH = 32;

    public String hashPassword(String password) throws Exception {
        byte[] salt = new byte[SALT_LENGTH];
        new SecureRandom().nextBytes(salt);

        PBEKeySpec spec = new PBEKeySpec(
            password.toCharArray(),
            salt,
            ITERATIONS,
            KEY_LENGTH
        );

        SecretKeyFactory factory =
            SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
        byte[] hash = factory.generateSecret(spec).getEncoded();

        // Clear sensitive data
        spec.clearPassword();

        // Store with iteration count for future upgrades
        return String.format("pbkdf2:sha256:%d:%s:%s",
            ITERATIONS,
            bytesToHex(salt),
            bytesToHex(hash));
    }
}
// SAFE: Node.js with strong password hashing
const bcrypt = require('bcrypt');
const argon2 = require('argon2');
const crypto = require('crypto');

// SAFE: bcrypt with appropriate work factor
const BCRYPT_ROUNDS = 12;

async function hashPasswordBcrypt(password) {
    return await bcrypt.hash(password, BCRYPT_ROUNDS);
}

async function verifyPasswordBcrypt(password, storedHash) {
    return await bcrypt.compare(password, storedHash);
}

// SAFE: Argon2 (recommended)
const ARGON2_OPTIONS = {
    type: argon2.argon2id,
    memoryCost: 65536,    // 64 MB
    timeCost: 3,          // iterations
    parallelism: 4,
    hashLength: 32
};

async function hashPasswordArgon2(password) {
    return await argon2.hash(password, ARGON2_OPTIONS);
}

async function verifyPasswordArgon2(password, storedHash) {
    try {
        if (await argon2.verify(storedHash, password)) {
            // Check if rehashing is needed
            if (argon2.needsRehash(storedHash, ARGON2_OPTIONS)) {
                return { valid: true, newHash: await hashPasswordArgon2(password) };
            }
            return { valid: true, newHash: null };
        }
        return { valid: false, newHash: null };
    } catch {
        return { valid: false, newHash: null };
    }
}

// SAFE: scrypt with strong parameters
const SCRYPT_OPTIONS = {
    N: 131072,  // CPU/memory cost (2^17)
    r: 8,       // Block size
    p: 1,       // Parallelization
    maxmem: 256 * 1024 * 1024  // 256 MB max memory
};

async function hashPasswordScrypt(password) {
    const salt = crypto.randomBytes(32);

    return new Promise((resolve, reject) => {
        crypto.scrypt(password, salt, 64, SCRYPT_OPTIONS, (err, derivedKey) => {
            if (err) reject(err);
            resolve(`scrypt:${SCRYPT_OPTIONS.N}:${SCRYPT_OPTIONS.r}:${SCRYPT_OPTIONS.p}:` +
                   `${salt.toString('hex')}:${derivedKey.toString('hex')}`);
        });
    });
}

// SAFE: PBKDF2 with high iteration count
const PBKDF2_ITERATIONS = 600000;

async function hashPasswordPBKDF2(password) {
    const salt = crypto.randomBytes(32);

    return new Promise((resolve, reject) => {
        crypto.pbkdf2(password, salt, PBKDF2_ITERATIONS, 32, 'sha256', (err, key) => {
            if (err) reject(err);
            resolve(`pbkdf2:sha256:${PBKDF2_ITERATIONS}:${salt.toString('hex')}:${key.toString('hex')}`);
        });
    });
}

Exploited in the Wild

RockYou Breach (2009)

32 million passwords stored in plain text were breached, demonstrating the importance of proper password hashing. This breach provided password lists still used today.

LinkedIn Breach (2012)

6.5 million SHA-1 hashed passwords (no salt) were cracked within hours using GPU acceleration, affecting many more in the full 117 million account breach revealed in 2016.

Ashley Madison (2015)

37 million accounts were breached. While they used bcrypt, older accounts had MD5 hashes which were quickly cracked.


Tools to test/exploit


CVE Examples


References

  1. MITRE. "CWE-916: Use of Password Hash With Insufficient Computational Effort." https://cwe.mitre.org/data/definitions/916.html

  2. OWASP. "Password Storage Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html