Back to Blog
high SEVERITY6 min read

How Weak bcrypt Salt Rounds Happen in Node.js and How to Fix It

A critical password hashing weakness was discovered in the authentication controller where bcrypt was configured with only 10 salt rounds instead of the recommended minimum of 12. This configuration made user passwords significantly more vulnerable to brute-force attacks if an attacker gained access to the password hash database. The fix was a simple but impactful one-line change that doubles the computational cost required to crack passwords.

O
By Orbis AppSec
Published August 16, 2026Reviewed August 16, 2026

Answer Summary

This vulnerability involves insufficient bcrypt salt rounds (CWE-916) in a Node.js authentication controller. The `saltRounds` constant was set to 10, which provides inadequate protection against modern GPU-based password cracking. The fix increases `saltRounds` from 10 to 12 in `authController.js`, quadrupling the computational work required to crack each password hash.

Vulnerability at a Glance

cweCWE-916
fixChanged `saltRounds` from 10 to 12 in `backend/controllers/authController.js`
riskAttackers can crack weak passwords 4x faster with salt rounds of 10 vs 12
languageJavaScript (Node.js)
root cause`saltRounds = 10` provides insufficient computational cost against GPU attacks
vulnerabilityInsufficient Password Hashing Work Factor

Introduction

The backend/controllers/authController.js file handles all user authentication logic including registration and login, but a subtle configuration flaw in the password hashing setup created a significant security risk. On line 10, the saltRounds constant was set to 10—a value that was once considered adequate but now falls short against modern GPU-accelerated password cracking techniques.

This matters because if an attacker ever gains access to your MongoDB database through a separate vulnerability (SQL injection, misconfigured access controls, or a data breach), the strength of your password hashes becomes your last line of defense. With salt rounds of 10, that defense was weaker than it should be.

The Vulnerability Explained

bcrypt is a password hashing function designed to be computationally expensive, making brute-force attacks impractical. The "salt rounds" parameter (also called the work factor or cost factor) determines how many iterations the algorithm performs. Each increment doubles the computational work required.

Here's the vulnerable configuration that was in production:

const saltRounds = 10;

At first glance, this seems reasonable—10 salt rounds was the standard recommendation for years. However, the security landscape has changed dramatically:

Why 10 Salt Rounds Is No Longer Sufficient

The computational cost of bcrypt with 10 rounds can be expressed as 2^10 = 1,024 iterations. Modern GPUs can test millions of password candidates per second at this work factor. According to security research, a single high-end GPU can crack bcrypt hashes at approximately:

  • 10 rounds: ~5,000 hashes/second
  • 12 rounds: ~1,250 hashes/second

This means an attacker with access to your password hashes could crack weak passwords (common words, short passwords, predictable patterns) in hours rather than days.

Attack Scenario Specific to This Application

Consider this exploitation path for the affected application:

  1. Database Access: An attacker exploits a MongoDB injection vulnerability or gains access through a misconfigured cloud database instance
  2. Hash Extraction: They dump the users collection containing bcrypt password hashes
  3. Offline Cracking: Using a GPU cluster, they run dictionary attacks against the hashes
  4. Account Takeover: With salt rounds of 10, common passwords like "Summer2024!" or "Company123" crack within hours
  5. Persistent Access: Given the TOKEN_EXPIRY = "7d" setting visible in the code, compromised accounts provide week-long access windows

The combination of the 7-day JWT expiration and weak password hashing created a compounding risk—attackers had both easier password cracking AND longer exploitation windows.

The Fix

The fix was elegantly simple—a single character change with significant security implications:

Before

const saltRounds = 10;

After

const saltRounds = 12;

This change in backend/controllers/authController.js at line 10 increases the computational cost by a factor of 4 (2^12 / 2^10 = 4). Here's what this means in practice:

Metric Salt Rounds 10 Salt Rounds 12 Improvement
Iterations 1,024 4,096 4x more work
GPU crack rate ~5,000/sec ~1,250/sec 4x slower
Time to crack 1M hashes ~3.3 minutes ~13.3 minutes 4x longer

Why This Specific Change Works

The fix preserves complete backward compatibility—existing password hashes remain valid and users don't need to reset their passwords. When users next log in and their credentials are verified, new password changes will use the stronger work factor. Over time, as users update their passwords, the entire database migrates to the stronger configuration.

The change is also forward-looking. OWASP currently recommends a minimum of 10 rounds but suggests 12+ for sensitive applications. By choosing 12, this fix aligns with current best practices while maintaining acceptable login performance (bcrypt with 12 rounds typically completes in 200-400ms on modern server hardware).

Prevention & Best Practices

1. Establish Minimum Work Factor Standards

Create a security policy that mandates minimum bcrypt salt rounds:

// security-config.js
const SECURITY_CONSTANTS = {
  BCRYPT_MIN_ROUNDS: 12,
  BCRYPT_RECOMMENDED_ROUNDS: 13, // For high-security applications
};

// In your auth controller
const saltRounds = Math.max(
  SECURITY_CONSTANTS.BCRYPT_MIN_ROUNDS,
  parseInt(process.env.BCRYPT_ROUNDS) || 12
);

2. Implement Gradual Hash Upgrades

Add logic to upgrade password hashes on successful login:

async function loginUser(email, password) {
  const user = await User.findOne({ email });
  const isValid = await bcrypt.compare(password, user.passwordHash);

  if (isValid) {
    // Check if hash needs upgrading
    const hashRounds = bcrypt.getRounds(user.passwordHash);
    if (hashRounds < MINIMUM_ROUNDS) {
      const newHash = await bcrypt.hash(password, CURRENT_ROUNDS);
      await User.updateOne({ _id: user._id }, { passwordHash: newHash });
    }
  }
  return isValid;
}

3. Annual Security Reviews

Password hashing requirements evolve with hardware capabilities. Schedule annual reviews of your authentication configuration. What's secure today may be insufficient in 2-3 years.

4. Defense in Depth

Don't rely solely on password hashing strength:
- Implement rate limiting on login endpoints
- Use account lockout after failed attempts
- Enable multi-factor authentication
- Monitor for credential stuffing attacks
- Consider pepper values for additional hash security

Key Takeaways

  • The saltRounds = 10 configuration in authController.js was a silent vulnerability—the code worked correctly but provided insufficient protection against modern attacks
  • Each bcrypt salt round increment doubles computational cost—going from 10 to 12 rounds makes password cracking 4x harder
  • The 7-day JWT expiration (TOKEN_EXPIRY = "7d") amplified the risk—compromised passwords provided extended access windows
  • Existing password hashes remain valid after the fix—no user disruption, gradual security improvement as passwords are updated
  • Static analysis can catch this pattern—tools can flag bcrypt configurations below threshold values

How Orbis AppSec Detected This

  • Source: User password input during registration/login flows in authController.js
  • Sink: bcrypt.hash() call using saltRounds constant set to 10
  • Missing control: Insufficient work factor configuration—salt rounds below the recommended minimum of 12
  • CWE: CWE-916 (Use of Password Hash With Insufficient Computational Effort)
  • Fix: Increased saltRounds from 10 to 12 in backend/controllers/authController.js:10

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

This vulnerability demonstrates how security requirements evolve over time. Code that was secure when written can become vulnerable as attack capabilities improve. The bcrypt salt rounds configuration of 10 was once the standard recommendation, but GPU advances have made it insufficient for protecting against determined attackers.

The fix—changing a single number from 10 to 12—quadruples the difficulty of password cracking while maintaining full backward compatibility. It's a reminder that security isn't just about avoiding obvious mistakes; it's about staying current with evolving best practices.

Review your own authentication code today. If you're using bcrypt with fewer than 12 salt rounds, you have a similar vulnerability waiting to be exploited.

References

Frequently Asked Questions

What is insufficient password hashing work factor?

It's when a password hashing algorithm like bcrypt is configured with too few iterations, making password cracking computationally feasible for attackers with modern hardware.

How do you prevent weak bcrypt configuration in Node.js?

Always use a minimum of 12 salt rounds for bcrypt, and consider increasing this value as hardware improves. Review your authentication code annually.

What CWE is insufficient password hashing?

CWE-916: Use of Password Hash With Insufficient Computational Effort.

Is bcrypt alone enough to prevent password cracking?

No, bcrypt must be configured with sufficient salt rounds (12+). Even strong algorithms become weak with low work factors.

Can static analysis detect weak bcrypt configuration?

Yes, static analysis tools can flag bcrypt salt rounds below recommended thresholds by checking the numeric value passed to the hashing function.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #26

Related Articles

critical

How Command Injection via Unescaped Line Terminators Happens in Node.js and How to Fix It

A critical command injection vulnerability (CVE-2026-9277) was discovered in the shell-quote npm package version 1.8.3, where unescaped line terminators could allow attackers to execute arbitrary code. This fix upgrades shell-quote to version 1.9.0 using npm overrides to ensure all instances in the dependency tree are patched, eliminating the attack vector across the entire application.

critical

How Distributed Lock Takeover Happens in Node.js and How to Fix It

A critical vulnerability in `redis-lock/server.mjs` allowed any authenticated client to release another client's lock by guessing predictable holder identifiers like process IDs or hostnames. The fix implements cryptographically random `lockId` values that are minted on lock acquisition and validated on release, eliminating the exploit primitive entirely.

high

How Denial of Service via Infinite Loop happens in JavaScript (nanoid) and how to fix it

A high-severity denial of service vulnerability (CVE-2026-67213) was discovered in nanoid versions before 5.1.6 and 3.3.18, where the `customAlphabet` function could enter an infinite loop during random ID generation. The fix upgrades the transitive nanoid dependency from 3.3.16 to 3.3.18 using pnpm overrides, ensuring the vulnerable code path is eliminated from the entire dependency tree including PostCSS.

high

How Information Disclosure via Unstripped Credential Headers Happens in Electron Apps and How to Fix It

A high-severity vulnerability (CVE-2026-54673) in the builder-util-runtime package allowed sensitive credential headers to leak during HTTP redirects in Electron applications. The fix upgrades builder-util-runtime from version 9.5.1 to 9.7.0, which properly strips authentication headers before following redirects to prevent information disclosure.

high

How Command Injection happens in PHP and how to fix it

A high-severity command injection vulnerability was discovered in `lib/Controller/Helper.php` where the `corruptline()` method used `exec()` to run sed and awk commands with user-controlled input. The fix replaced all shell command execution with native PHP file operations using `SplFileObject`, eliminating the command injection attack surface entirely.

high

How Missing CSRF Middleware happens in Express.js and how to fix it

A high-severity CSRF vulnerability was discovered in `libProxy.js` of an Express.js application — the app had no CSRF middleware protecting its state-changing routes, leaving them open to cross-site request forgery attacks. The fix introduces a `csrf` token library, a `/csrf-token` endpoint to issue tokens, and a middleware that validates `x-csrf-token` headers or `_csrf` body fields on all non-safe HTTP methods. This proactive hardening removes an exploit primitive that could be chained with ot