Back to Blog
critical SEVERITY5 min read

How insufficient PBKDF2 iterations happen in JavaScript and how to fix it

A critical vulnerability in `libs/wgs/pbkdf2.js` used only 1 iteration for PBKDF2 password hashing, making passwords trivially crackable. The fix increases iterations to 600,000, aligning with OWASP 2023 recommendations and preventing GPU-accelerated brute-force attacks.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published September 9, 2026•Reviewed September 9, 2026

Answer Summary

This is a **CWE-916: Use of Password Hash With Insufficient Computational Effort** vulnerability in JavaScript's CryptoJS PBKDF2 implementation. The `libs/wgs/pbkdf2.js` file hardcoded `iterations: 1` in the PBKDF2 configuration, making password hashes 600,000 times faster to crack than recommended. The fix changes `iterations: 1` to `iterations: 600000` in the `cfg` object at line 18, bringing the implementation into compliance with OWASP password storage guidelines and preventing offline brute-force attacks.

Vulnerability at a Glance

cweCWE-916
fixIncreased iterations to 600,000 in CryptoJS PBKDF2 `cfg` object
riskTrivial password cracking via GPU-accelerated brute-force if database compromised
languageJavaScript
root causeHardcoded `iterations: 1` in PBKDF2 configuration instead of OWASP-recommended minimum
vulnerabilityInsufficient PBKDF2 iterations (weak password hashing)

How Insufficient PBKDF2 Iterations Happen in JavaScript and How to Fix It

Introduction

In the libs/wgs/pbkdf2.js file, we discovered a critical cryptographic vulnerability that rendered password hashing virtually useless. The PBKDF2 implementation—responsible for deriving encryption keys from passwords—was configured with iterations: 1, a value so low that an attacker with a modern GPU could test billions of password guesses per second.

This wasn't a subtle timing attack or complex exploit chain. It was a single hardcoded number that collapsed the security of the entire password storage system. The cfg object in the PBKDF2 extend block at line 18 contained this fatal flaw:

cfg:e.extend({keySize:4,hasher:j.SHA1,iterations:1})

For developers working with CryptoJS or similar cryptographic libraries, this case demonstrates why defaults can be dangerous and why every cryptographic parameter requires explicit, justified configuration.

The Vulnerability Explained

The Root Cause: One Fatal Default

PBKDF2 (Password-Based Key Derivation Function 2) is designed to be slow by design. It works by running a pseudorandom function (typically HMAC-SHA1) thousands or hundreds of thousands of times, making brute-force attacks prohibitively expensive.

The vulnerable code in libs/wgs/pbkdf2.js defined the default configuration like this:

n=j.PBKDF2=e.extend({cfg:e.extend({keySize:4,hasher:j.SHA1,iterations:1}),init:function(d){this.cfg=this.cfg.extend(d)},compute:function(e,b){...

The iterations: 1 setting means the HMAC-SHA1 operation runs exactly once. This eliminates PBKDF2's core defense mechanism.

Why This Matters: The Math of Password Cracking

With iterations: 1:
- A single RTX 4090 GPU can compute ~15 billion SHA1 operations per second
- An 8-character password using common characters falls in minutes
- A 10-character password falls in days

With iterations: 600,000 (OWASP 2023 recommendation):
- The same GPU manages ~25,000 password guesses per second
- The 8-character password now takes years
- The 10-character password takes millennia

Real-World Attack Scenario

Consider this exploitation path:

  1. Database compromise: An attacker exploits an SQL injection in the application's OAuth token storage (the plugins/auth-oauth2/src/store.ts mentioned in related findings) or obtains a backup
  2. Hash extraction: The attacker retrieves PBKDF2-derived keys stored as "encrypted" credentials
  3. Offline cracking: Using hashcat with a rule-based attack, the attacker tests billions of passwords per second against the iterations: 1 hashes
  4. Credential recovery: User passwords are recovered in bulk, enabling account takeover, lateral movement, and further data exfiltration

The getToken and setToken functions in the OAuth plugin were writing credentials to disk using this broken PBKDF2 implementation—meaning stored tokens were protected by cryptographic theater rather than real security.

The Fix

The remediation was surgical but transformative. The single-line change in libs/wgs/pbkdf2.js:

Before (vulnerable):

n=j.PBKDF2=e.extend({cfg:e.extend({keySize:4,hasher:j.SHA1,iterations:1}),init:fu

After (fixed):

n=j.PBKDF2=e.extend({cfg:e.extend({keySize:4,hasher:j.SHA1,iterations:600000}),init:fu
Aspect Before After
Iterations 1 600,000
Time per hash ~0.001 ms ~100 ms
GPU guesses/second ~15 billion ~25,000
Security Broken OWASP-compliant

This change brings the implementation in line with OWASP Password Storage Cheat Sheet recommendations, which specify:

PBKDF2-SHA1: 600,000 iterations (minimum as of 2023)

The fix preserves all existing behavior—key size remains 4 words (128 bits), SHA1 remains the hash function—while restoring the computational cost that makes password hashing effective.

Prevention & Best Practices

1. Never Trust Cryptographic Defaults

CryptoJS's default iterations: 1 dates from an era when JavaScript performance in browsers was severely limited. Modern applications must explicitly override such defaults:

// Always specify iterations explicitly
const key = CryptoJS.PBKDF2(password, salt, {
    keySize: 256/32,
    iterations: 600000,  // Explicit, justified value
    hasher: CryptoJS.algo.SHA256  // Prefer SHA-256 when possible
});

2. Migrate to Modern Algorithms

While fixing PBKDF2 iterations is essential, consider migrating to Argon2id (winner of the Password Hashing Competition) for new implementations:

// Using argon2-browser or server-side Node.js
const hash = await argon2.hash(password, {
    type: argon2.argon2id,
    memoryCost: 65536,  // 64 MB
    timeCost: 3,
    parallelism: 4
});

3. Implement Iteration Count Verification

Add runtime checks to prevent regression:

function validatePBKDF2Config(config) {
    const MIN_ITERATIONS = 600000;
    if (!config.iterations || config.iterations < MIN_ITERATIONS) {
        throw new Error(`PBKDF2 iterations must be >= ${MIN_ITERATIONS}`);
    }
    return config;
}

4. Security Scanning Integration

Configure static analysis tools to flag low iteration counts:

  • Semgrep: Use rules like javascript.lang.security.audit.crypto.pbkdf2-weak-iteration
  • ESLint: Custom rules for CryptoJS configuration patterns
  • Dependency scanning: Flag outdated CryptoJS versions with known weak defaults

Key Takeaways

  • The iterations: 1 default in libs/wgs/pbkdf2.js provided no meaningful protection—password hashes were computationally equivalent to raw SHA1, crackable at billions of guesses per second
  • Always verify cryptographic parameters rather than accepting library defaults, especially in legacy libraries like CryptoJS where defaults may reflect outdated threat models
  • The 600,000-iteration minimum is not arbitrary—it represents the current balance between attacker capability (GPUs, ASICs) and legitimate user experience
  • Token storage in plugins/auth-oauth2/src/store.ts was compromised by this flaw—any "encrypted" credentials written using this PBKDF2 implementation should be considered at risk and rotated
  • CryptoJS PBKDF2 with SHA1 remains viable only with sufficient iterations—prefer SHA-256 variants or modern algorithms like Argon2id for new development

How Orbis AppSec Detected This

Element Details
Source Hardcoded configuration object in libs/wgs/pbkdf2.js
Sink PBKDF2.compute() function using g.iterations from cfg object
Missing control No minimum iteration validation; default value of 1 used without override
CWE CWE-916: Use of Password Hash With Insufficient Computational Effort
Fix Changed iterations:1 to iterations:600000 in the cfg extend block

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

The libs/wgs/pbkdf2.js vulnerability demonstrates how a single numeric constant can undermine an entire security architecture. The change from iterations: 1 to iterations: 600000 transforms a broken implementation into an OWASP-compliant defense—reducing feasible attack rates from billions to thousands of attempts per second.

For teams maintaining legacy JavaScript applications, this case underscores the importance of auditing cryptographic configurations, not just algorithm choices. Modern threats require modern parameters, and security is only as strong as the weakest default.

References

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1

Related Articles

high

SQLite Auth Database Plaintext Storage in InitAuthDB()

The `InitAuthDB()` function opened SQLite databases without restricting filesystem permissions, leaving bcrypt password hashes, session tokens, and user settings exposed to any local account with read access. The fix explicitly sets `os.Chmod(filepath, 0600)` immediately after database creation, ensuring only the owner can access authentication secrets.

critical

Hybridauth Telegram OAuth Timing Attack in `authenticateCheckError()`

The Telegram OAuth provider in Hybridauth relied on `strcmp()` to validate HMAC-SHA256 signatures, exposing a timing side-channel that could allow attackers to forge authentication tokens character by character. The fix replaces this with PHP's timing-safe `hash_equals()` function, eliminating the information leak.

high

undici 7.29.0 TLS Bypass: BalancedPool Drops Connect Options

A critical flaw in undici's BalancedPool implementation silently discards TLS certificate validation settings when routing requests through certain connection paths. Attackers on the network could intercept HTTPS traffic without triggering validation errors. The fix preserves connect options throughout the connection lifecycle.

critical

Model Fetching Without Integrity Verification in ONNX Loading

A machine learning application was fetching ONNX model files and chunks over the network without verifying their integrity, creating an opening for model poisoning attacks. The fix adds cryptographic integrity verification at the point where downloaded chunks are reassembled and cached, ensuring models have not been modified in transit or at rest.

high

nanoid 3.3.11 Integer Overflow: Predictable ID Generation

An integer overflow in nanoid 3.3.11's internal randomness generation causes the library to fall back to predictable ID sequences, undermining the cryptographic guarantees of its supposedly unguessable identifiers. The fix upgrades the dependency tree to patched versions 3.3.12 or 5.1.11.

high

generateUUID() Ditches MD5 for SHA-256 to Fix CWE-328

The `generateUUID()` helper built request-identifying UUIDs by hashing an input string with MD5, a cryptographically broken algorithm susceptible to collisions. The fix swaps the hash function for SHA-256, reducing the chance that two different inputs produce the same generated identifier.