Skip to content

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

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions