Description
Fast general-purpose hashes such as MD5 or SHA-1 let an attacker test password guesses rapidly with GPUs or cloud resources after obtaining the hashes. Missing salts or low iteration and memory costs increase the risk. An attacker may recover passwords and take over accounts, commonly after a database or backup leak followed by bulk cracking with tools such as Hashcat.
Potential impact
- Recovered passwords may allow unauthorized account access.
- A compromised administrative account may give an attacker control over systems.
- Account access may expose sensitive data.
- Recovered passwords may be reused in credential-stuffing attempts against other services.
- Weak storage may undermine compliance and user trust.
Remediation
- Prefer Argon2id for new password storage, or consider scrypt when unavailable. Use bcrypt for legacy compatibility or PBKDF2 where requirements call for it.
- Set and benchmark work factors using current guidance. Examples are Argon2id
m≥19 MiB, t≥2, p=1; scryptN≥2^17, r=8, p=1; bcryptcost≥10; or PBKDF2-HMAC-SHA256 with at least600,000iterations. - Use a unique random salt for each password. Let the library generate and store it when that feature is provided.
- If using a pepper, keep it in a secret store or HSM separate from the password database. It does not replace a salt or sufficient work factor.
- After a successful login, rehash weak existing hashes with the current algorithm and parameters.
Examples
This comparison retains bcrypt for an existing system. bcrypt uses only the first 72 bytes of UTF-8 input: enforce a consistent policy at registration and verification, and do not let long passwords be silently truncated. Cost 12 is illustrative and must be benchmarked in the deployment environment.
Before
javascript
// Store passwords with a fast general-purpose hash (MD5/SHA-1)
const crypto = require("crypto");
async function saveUserWeak(username, plainPassword, db) {
const digest = crypto
.createHash("md5")
.update(plainPassword, "utf8")
.digest("hex");
await db.users.insert({ username, passwordHash: digest });
}
After
javascript
// Use bcrypt with unique salts and an appropriate cost
const bcrypt = require("bcrypt");
async function saveUserSecure(username, plainPassword, db) {
const cost = 12; // Example cost factor; benchmark in the deployment environment
const hash = await bcrypt.hash(plainPassword, cost); // Includes a randomly generated salt
await db.users.insert({ username, passwordHash: hash });
}
async function verifyLogin(username, inputPassword, db) {
const user = await db.users.findOne({ username });
if (!user) return false;
return bcrypt.compare(inputPassword, user.passwordHash);
}
Explanation:
- Before: MD5 is designed for speed, allowing many guesses on GPUs or ASICs. Without salts or a work factor, it is especially vulnerable to guessing and precomputed-table attacks.
- After: bcrypt generates a salt for each password and increases the computation required for each guess. The example uses cost 12 and verifies with
bcrypt.compare. Apply a strong password policy and limits on login attempts as well.