Back to Blog
medium SEVERITY5 min read

How Insufficient Password Hashing Cost Factor Happens in Node.js and How to Fix It

A bcrypt password hashing implementation in the User.js model was using a cost factor of 10, which falls below OWASP's 2024 recommendation of 12 for applications handling sensitive data. This fix upgrades the salt rounds from 10 to 12, increasing the computational work required to crack passwords by approximately 4x, significantly improving protection against brute-force attacks on this e-commerce platform.

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

Answer Summary

Insufficient bcrypt cost factor (CWE-916) occurs in Node.js when password hashing uses fewer salt rounds than recommended—in this case, cost factor 10 instead of the OWASP-recommended minimum of 12 for 2024. The fix involves changing `bcrypt.genSalt(10)` to `bcrypt.genSalt(12)` in the User model's `hashPassword()` method, which increases computational work from ~1024 to ~4096 iterations, making brute-force attacks significantly harder.

Vulnerability at a Glance

cweCWE-916
fixIncrease bcrypt salt rounds from 10 to 12 in User.hashPassword()
riskWeakened protection against offline brute-force password attacks
languageJavaScript (Node.js)
root causebcrypt cost factor of 10 provides insufficient computational work for modern hardware
vulnerabilityInsufficient Password Hashing Cost Factor

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:

  1. GPU acceleration: Modern GPUs can test millions of password candidates per second against bcrypt hashes with cost factor 10
  2. Cloud computing: Attackers can rent massive compute clusters cheaply for password cracking
  3. 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:

  1. An attacker exploits an unrelated SQL injection vulnerability to dump the users table
  2. They extract bcrypt hashes from user accounts, including those with stored payment methods
  3. Using a modern GPU cluster (rentable for ~$3/hour), they can test approximately 28,000 bcrypt cost-10 hashes per second
  4. Common passwords like "Password123!" would be cracked within hours
  5. 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.

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 in backend/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.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1473

Related Articles

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

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 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.