Introduction
The backend/models/User.js file handles user authentication for an e-commerce platform, including the critical task of hashing passwords before storage. At line 149, the hashPassword() static method was using bcrypt with a cost factor of 10—a configuration that, while not immediately exploitable, falls short of modern security standards for applications handling financial data.
// The vulnerable pattern at line 152
const salt = await bcrypt.genSalt(10);
This matters because password hashing cost factors aren't just arbitrary numbers—they directly determine how much computational work an attacker must perform to crack each password hash. For developers building authentication systems, understanding this relationship is crucial for protecting user credentials.
The Vulnerability Explained
Bcrypt's cost factor (also called "salt rounds" or "work factor") determines the number of iterations used in the hashing algorithm. The relationship is exponential: cost factor 10 means 2^10 = 1,024 iterations, while cost factor 12 means 2^12 = 4,096 iterations.
Here's the vulnerable code from User.js:
/**
* Hash a password
* @param {string} password - Plain text password
* @returns {Promise<string>} Hashed password
*/
static async hashPassword(password) {
const salt = await bcrypt.genSalt(10); // Only 1,024 iterations
return await bcrypt.hash(password, salt);
}
Why Cost Factor 10 Is No Longer Sufficient
In 2024, OWASP recommends a minimum cost factor of 12 for several reasons:
- GPU acceleration: Modern GPUs can test millions of password candidates per second against bcrypt hashes with cost factor 10
- Cloud computing: Attackers can rent massive compute clusters cheaply for password cracking
- Password database breaches: If this e-commerce platform's database is compromised, every user password becomes a target
Attack Scenario Specific to This Application
Consider this realistic attack chain:
- An attacker exploits an unrelated SQL injection vulnerability to dump the
userstable - They extract bcrypt hashes from user accounts, including those with stored payment methods
- Using a modern GPU cluster (rentable for ~$3/hour), they can test approximately 28,000 bcrypt cost-10 hashes per second
- Common passwords like "Password123!" would be cracked within hours
- With cracked credentials, attackers access user accounts containing saved credit cards and order history
With cost factor 12, the same attack would take approximately 4x longer, making it significantly more expensive and time-consuming.
The Fix
The fix is elegantly simple but has profound security implications. In backend/models/User.js at line 152, the cost factor was increased from 10 to 12:
Before (Vulnerable)
static async hashPassword(password) {
const salt = await bcrypt.genSalt(10);
return await bcrypt.hash(password, salt);
}
After (Secure)
static async hashPassword(password) {
const salt = await bcrypt.genSalt(12);
return await bcrypt.hash(password, salt);
}
What This Change Accomplishes
| Metric | Cost Factor 10 | Cost Factor 12 |
|---|---|---|
| Iterations | 1,024 | 4,096 |
| Relative cracking time | 1x | 4x |
| OWASP 2024 compliant | ❌ No | ✅ Yes |
The change affects only new password hashes (new user registrations and password changes). Existing hashes remain valid—bcrypt stores the cost factor within the hash itself, so bcrypt.compare() automatically uses the correct cost factor for verification.
Performance Consideration
Increasing the cost factor does increase hashing time:
- Cost 10: ~100ms per hash
- Cost 12: ~400ms per hash
For an authentication endpoint, this 300ms increase is acceptable and actually provides additional protection against credential stuffing attacks by rate-limiting login attempts naturally.
Prevention & Best Practices
1. Follow OWASP Password Storage Guidelines
OWASP recommends reviewing your bcrypt cost factor annually. As of 2024, the minimum is 12, but consider 13-14 for high-value targets.
// Recommended: Use a configurable constant
const BCRYPT_COST_FACTOR = parseInt(process.env.BCRYPT_COST) || 12;
static async hashPassword(password) {
const salt = await bcrypt.genSalt(BCRYPT_COST_FACTOR);
return await bcrypt.hash(password, salt);
}
2. Implement Progressive Hash Upgrades
When users log in with older hashes, rehash with the current cost factor:
async function verifyAndUpgrade(userId, password, storedHash) {
const isValid = await bcrypt.compare(password, storedHash);
if (!isValid) return false;
// Check if hash needs upgrading
const rounds = bcrypt.getRounds(storedHash);
if (rounds < BCRYPT_COST_FACTOR) {
const newHash = await User.hashPassword(password);
await User.updateHash(userId, newHash);
}
return true;
}
3. Use Security Linters
Configure ESLint with security plugins to flag weak cryptographic parameters:
{
"plugins": ["security"],
"rules": {
"security/detect-weak-crypto": "error"
}
}
Key Takeaways
- Cost factor 10 provides only 1,024 iterations—insufficient for e-commerce platforms handling financial data in 2024
- The
User.hashPassword()method is the single point where password security is determined; always audit this function - Bcrypt's cost factor should be reviewed annually as hardware capabilities increase
- Existing password hashes remain valid after upgrading the cost factor—bcrypt handles this automatically
- A 4x increase in cracking difficulty (from cost 10 to 12) significantly raises the bar for attackers who obtain database dumps
How Orbis AppSec Detected This
- Source: User-supplied password input to the
hashPassword()method - Sink:
bcrypt.genSalt(10)call inbackend/models/User.js:152 - Missing control: Cost factor below OWASP's recommended minimum of 12 for 2024
- CWE: CWE-916 (Use of Password Hash With Insufficient Computational Effort)
- Fix: Increased bcrypt salt rounds from 10 to 12 in the
User.hashPassword()static method
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
Password hashing cost factors might seem like minor configuration details, but they represent a critical security control. The difference between cost factor 10 and 12 in this User.js model means the difference between passwords that can be cracked in hours versus days—a meaningful deterrent for attackers.
For developers building authentication systems, remember: bcrypt is only as strong as its configuration. Review your cost factors annually, implement progressive hash upgrades, and always follow current OWASP recommendations for the type of data your application handles.