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

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How dependabot-missing-cooldown happens in GitHub Actions/Node.js and how to fix it

The repository's `.github/dependabot.yml` had no cooldown period configured, meaning Dependabot could immediately propose updates to newly published package versions with zero time for the community to flag malware or instability. The fix adds a `cooldown` block with `default-days: 7` to both the `npm` and `github-actions` ecosystems, forcing a 7-day waiting period before new releases are surfaced as update PRs.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.

critical

How Remote Code Execution Happens in Handlebars Template Compilation and How to Fix It

CVE-2026-33937 is a critical remote code execution vulnerability in Handlebars.js that allows attackers to execute arbitrary code by passing maliciously crafted Abstract Syntax Tree (AST) objects to the compile() function. The vulnerability was patched in version 4.7.9, and we've upgraded to protect against this threat vector.