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.

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

References

Frequently Asked Questions

What is insufficient password hashing cost factor?

It occurs when a password hashing algorithm uses fewer iterations than recommended, making hashed passwords easier to crack through brute-force attacks if the database is compromised.

How do you prevent insufficient password hashing in Node.js?

Use bcrypt with a cost factor of at least 12 (OWASP 2024 recommendation), and periodically review and increase the cost factor as hardware improves.

What CWE is insufficient password hashing cost factor?

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

Is using bcrypt enough to prevent weak password hashing?

No, bcrypt must be configured with an adequate cost factor. Using bcrypt with a low cost factor (like 10) still leaves passwords vulnerable to modern GPU-based cracking.

Can static analysis detect insufficient password hashing cost factor?

Yes, static analysis tools can flag bcrypt.genSalt() calls with cost factors below recommended thresholds, though the specific threshold may need configuration.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1473

Related Articles

critical

How Sensitive Data Exposure in Error Logging happens in TypeScript/Deno and how to fix it

A critical vulnerability in Supabase Edge Functions allowed sensitive authentication errors and API credentials to leak through verbose error logging. The `cancel-subscription/index.ts` function logged full error objects to the console, potentially exposing Paddle API keys and auth tokens in deployment logs. The fix sanitizes all error messages to log only safe error text while preserving debugging capability.

critical

How HTTP Header Injection Happens in Go and How to Fix It

A critical vulnerability in the file upload handler allowed attackers to inject CRLF sequences into HTTP response headers through crafted filenames. The fix sanitizes user-supplied filenames before using them in Content-Disposition headers, preventing header injection attacks that could lead to cache poisoning, session fixation, or XSS.

high

How Path Traversal and Security Policy Bypass Happens in Node.js Dependencies and How to Fix It

A high-severity vulnerability in the fast-uri package (CVE-2026-6321) allowed attackers to bypass security policies through improper Unicode hostname canonicalization and path traversal. This issue affected the @apralabs/apra-fleet project through its dependency tree, and was resolved by upgrading fast-uri from version 3.1.0 to 4.1.2 using npm overrides.

high

How Command Injection happens in Node.js child_process calls and how to fix it

A high-severity command injection vulnerability was discovered in `tools/utils/lang/helpers.ts` where the `prettier()` function passed a user-controllable `fileName` argument directly into a shell command string via `exec()`. By replacing `exec()` with `execFile()` and passing arguments as an array, the fix eliminates shell interpolation entirely, preventing attackers from injecting arbitrary shell commands through malicious filenames.

high

How Quadratic CPU Consumption in YAML Parsing happens in JavaScript and how to fix it

A high-severity vulnerability in js-yaml versions 3.x and 4.x allowed attackers to cause quadratic CPU consumption through specially crafted YAML documents using the `!!omap` type. This denial-of-service vulnerability (GHSA-5p4m-2wfm-xmqj) was fixed by upgrading from js-yaml 4.3.0 to 4.3.1, protecting applications from algorithmic complexity attacks during YAML parsing.

high

How Arbitrary HTTP Header Injection via Prototype Pollution happens in JavaScript and how to fix it

A high-severity vulnerability (CVE-2026-42035) in axios version 1.13.5 allowed attackers to inject arbitrary HTTP headers through prototype pollution. The fix upgrades axios to version 1.18.0 in the frontend's dependency tree, which includes proper prototype chain validation when constructing HTTP request headers. This prevents attackers from manipulating outgoing requests to perform SSRF, session hijacking, or cache poisoning attacks.