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, 2026•Reviewed 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

high

js-yaml 4.3.1 Denial of Service: Malformed Input Hangs YAML Parser

A denial of service vulnerability in js-yaml versions 4.3.1 and earlier allows attackers to hang the YAML parser indefinitely by providing specially crafted malformed input. The fix, released in versions 4.3.2 and 3.15.2, patches the parsing logic to prevent unbounded processing. Upgrading is recommended for all applications parsing untrusted YAML data.

high

Express `app.get('*')` Wildcard Handler Path Traversal in watch.js

A first-party Express server's wildcard route handler used `req.url.indexOf('font.woff2')` to gate access to a font file, allowing attackers to bypass the substring check with crafted paths. The fix replaces the catch-all handler with explicit route registration.

critical

Updater.parseUpdate() CWE-494: Unsigned Metadata Download

The parseUpdate function in the Updater component extracted download URLs from remote server responses without cryptographic verification, enabling supply chain attacks via compromised or spoofed update servers. The fix adds strict URL validation requiring HTTPS and a trusted hostname before accepting any update metadata.

high

brace-expansion DoS: Exponential Backtracking in Nested Brace Patterns

A critical vulnerability in brace-expansion allows attackers to cause denial of service by submitting specially crafted patterns with nested braces. The exponential-time complexity in pattern expansion creates a computationally expensive path that can freeze applications processing user-controlled input.

high

CVE-2026-67213: nanoid customAlphabet Infinite Loop Fix

nanoid, a widely-used ID generator pulled in transitively through postcss and vitepress, had an infinite-loop bug in its `customAlphabet` code path before version 5.1.6. This PR pins the entire dependency tree to nanoid 5.1.16 via a pnpm override so no transitive consumer can resolve back to the vulnerable 3.3.16 release.

high

KNX Project Extractor ZIP Bomb: Unbounded Decompression Before Size

The KNX project extractor used `@zip.js/zip.js` to decompress .knxproj files without enforcing maximum entry sizes, total archive sizes, or compression ratios. This allowed attackers to upload ZIP bombs that expanded exponentially—like the famous 42.zip producing 4.5PB from 42KB—consuming all available memory before the existing `Checked` validation could trigger. The fix introduces three hard limits: 512MB per entry, 1GB total per archive, and a 100:1 compression ratio ceiling.