Skip to content

Security: README AES examples use EVP_BytesToKey (MD5, 1 iteration) without disclosure #534

@ekreloff

Description

@ekreloff

Summary

The README's AES encryption examples use passphrase-based encryption:

var ciphertext = CryptoJS.AES.encrypt('my message', 'secret key 123').toString();

When a string passphrase is passed, crypto-js internally routes through PasswordBasedCipherOpenSSLKdfEvpKDF, which derives the AES key using:

  • Hash: MD5 — cryptographically broken since 2004
  • Iterations: 1 — provides zero resistance against brute force
  • Cipher mode: CBC — no authentication (vulnerable to padding oracle attacks)

The README does not mention any of these implementation details. Developers copying the example get encryption where a GPU can test billions of candidate passphrases per second (single MD5 hash), and the ciphertext has no integrity protection.

What's happening internally

Tracing the code path in src/cipher-core.js and src/evpkdf.js:

  1. typeof key == 'string' → selects PasswordBasedCipher
  2. PasswordBasedCipher.encrypt() calls cfg.kdf.execute(password, cipher.keySize, cipher.ivSize, cfg.salt, cfg.hasher)
  3. OpenSSLKdf.execute() uses EvpKDF with default config: { hasher: MD5, iterations: 1 }
  4. Block cipher defaults: { mode: CBC, padding: Pkcs7 } — no authentication

Why this matters

  • 15.6M weekly downloads — this is the most-downloaded encryption library in the npm ecosystem
  • The library is discontinued — these insecure examples will remain indefinitely
  • The README is the primary reference — the gitbook docs also contain no security warnings about the KDF
  • Issue What is the algorithm that generates AES256bits key from passphrase? #370 ("What is the algorithm that generates AES256bits key from passphrase?") shows developers are already confused about what happens internally

The irony

The v4.2.0 release notes say: "Change default hash algorithm and iteration's for PBKDF2 to prevent weak security by using the default configuration." The team recognized weak KDF defaults as a security issue — but only fixed the standalone PBKDF2 module. The AES passphrase encryption path still uses EvpKDF with MD5/1-iteration.

Similarly, v4.0.0 replaced Math.random() with native crypto for random number generation. The library's RNG is now cryptographically secure — but the key derivation that feeds into AES encryption is a single MD5 hash, making the improved RNG irrelevant for passphrase-based encryption.

CWEs

  • CWE-328: Use of Weak Hash (MD5 for key derivation)
  • CWE-916: Use of Password Hash With Insufficient Computational Effort (1 iteration)
  • CWE-354: Improper Validation of Integrity Check Value (CBC without authentication)

Suggested documentation fix

Add a security note to the AES examples:

⚠️ Security Note: When passing a string passphrase, crypto-js derives the AES key using OpenSSL's EVP_BytesToKey with MD5 and 1 iteration. This provides minimal resistance against brute force. For production use, either:

  • Derive keys explicitly using CryptoJS.PBKDF2() with SHA-256 and ≥600,000 iterations, then pass the derived WordArray as the key
  • Use the native crypto module with crypto.scrypt() or crypto.pbkdf2() for key derivation, and crypto.createCipheriv() with aes-256-gcm for authenticated encryption

Context

This issue is part of a broader pattern documented across npm libraries: libraries with secure code improvements that still teach insecure patterns in their documentation. Analysis: The Documentation Attack Surface

Previously filed:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions