Almost six million dollars walked out of cryptocurrency wallets this summer because of a JavaScript function most developers have never opened. The function is CryptoJS.lib.WordArray.random(), part of the crypto-js library that millions of projects still ship, and at its core it leans on Math.random(). Coinspect, the blockchain security firm behind the Ill Bloom investigation, published that finding this week, and the numbers are hard to argue with: two drain sweeps since late May, a measured lower bound of $5,690,922, and five wallet apps confirmed to have used the generator for recovery-phrase entropy.

Scattered dice representing random number generation and weak randomness in JavaScript cryptography
Image: Dietmar Rabich via Wikimedia Commons (CC BY-SA 4.0)

This isn’t a story about exotic side channels or nation-state attackers. It’s about a twelve-year-old code path, a weak random number generator, and what happens when developers assume a dependency that calls itself a cryptography library is doing cryptography properly. If you write JavaScript, this one is aimed at you.

What actually broke

Every self-custody wallet starts with a recovery phrase, usually 12 or 24 words. Those words are supposed to be pulled from a search space so vast that guessing them is hopeless. The affected wallets were not that random.

Coinspect traced the problem to CryptoJS.lib.WordArray.random(), as detailed in its disclosure this week. In vulnerable releases of crypto-js, that function is backed by a Multiply-With-Carry (MWC) generator that gets its seed from Math.random(). Math.random is fine for shuffling a list or picking a quiz question. It is not fine for generating the key to your money.

The firm’s analysis makes the collapse concrete: a request for 128-bit entropy should yield a search space of 2^128, and 256-bit should give 2^256. The vulnerable generator reduced those to roughly 2^39 and 2^47 — small enough to enumerate on ordinary hardware. An attacker who knows the generator and the weak seed can walk through every possible phrase, derive the addresses, and check the blockchain for wallets still holding funds. That is exactly what happened.

The five wallets, and the fix situation

Coinspect has now named the five applications it says used the vulnerable generator as an entropy source:

  • RRWallet — discontinued. No fix.
  • Bexo Wallet — fixed in version 20.1.0, though the updated builds had not been uploaded as of the disclosure.
  • NanChat — affected before 1.3.0, fixed in 1.3.0. So far the only app with a public advisory.
  • Bitcoin Libre — fixed in version 4, released July 2024.
  • Milo — discontinued. No fix.

The maintainer of crypto-js published advisory GHSA-rg76-677x-56q9 on August 5 with a Critical rating and a CVSS score of 9.0. It lists every release below 4.0.0 as affected, with an important nuance: releases 3.2.0 and 3.2.1 briefly used native cryptographic randomness, but 3.3.0 restored the weak code because the change was considered breaking. Version 4.0.0, released February 2020, restored native randomness for good. So an upgrade within the 3.x line could actually move a project from a fixed release onto a vulnerable one.

Here is the part that should make every developer pause: once a recovery phrase has been generated from that weak output, no amount of hashing, PBKDF2 processing, or later package updates can restore the missing entropy. The damage is permanent. Updating the app does not repair an existing phrase — the phrase stays guessable wherever it is imported, even into a hardware wallet.

How to audit your own project

You do not need to be building a wallet for this to matter to you. Any project that uses crypto-js to generate keys, IVs, salts, or tokens — for AES encryption, for password hashing, for anything security-sensitive — inherits whatever entropy that function provides. Here is the audit I would run on every JavaScript project this week.

Step 1: find crypto-js in your dependency tree

First, see whether you depend on it at all, and what version resolved:

npm ls crypto-js

You may be surprised. Transitive dependencies are how this kind of thing sneaks in — crypto-js can arrive as a sub-dependency of an SDK or a helper library you installed years ago. The five wallet apps did not all call the function directly; Coinspect identified ferrumnet/bip39, a React Native fork that replaced upstream bip39’s native cryptographic randomness with CryptoJS, as one route into wallet software. Not the only one.

Step 2: let npm audit do the dirty work

On a test project with crypto-js 3.3.0 installed, npm audit flags it immediately:

crypto-js  <=4.1.1
Severity: critical
crypto-js: Insufficient Entropy in Cryptographic Secret Generation via
Vulnerable CryptoJS Dependency Chain - GHSA-rg76-677x-56q9

fix available via `npm audit fix --force`
Will install [email protected], which is a breaking change

Note the fix path: the advisory’s affected range is everything below 4.0.0, and npm’s remediation jumps straight to 4.2.0. If your project sits on 3.x, the fix is a major version bump, which is exactly the kind of upgrade that teams put off until someone audits the lockfile.

Step 3: grep for the dangerous call

An outdated crypto-js alone is not necessarily exploitable — the advisory is explicit that carrying the dependency does not make you vulnerable; using the vulnerable function for security-sensitive values does. So search your source:

grep -rn "WordArray.random" src/ test/ scripts/

Also search for the patterns that usually sit behind it:

grep -rn "random(16)\|random(32)\|random(64)" src/

If you find WordArray.random() being used to build a key, an IV, a salt, a session token, or a seed of any kind, treat it as a finding, not a false positive. Aes, Hmac, and the other crypto-js primitives are not the problem here — the random generator is.

The proof, in one command

I built a test harness with crypto-js 3.3.0 and 4.2.0 side by side and ran the simplest experiment I could think of: pin Math.random to a constant and see what happens to the “random” output.

const CryptoJS = require('crypto-js');
Math.random = () => 0.5;   // attacker-controlled, predictable

const a = CryptoJS.lib.WordArray.random(16).toString();
const b = CryptoJS.lib.WordArray.random(16).toString();
console.log(a === b);  // true on crypto-js 3.3.0

On crypto-js 3.3.0, both calls return the exact same 32-character hex string. The output is fully deterministic once the seed source is known. On 4.2.0, the same test returns two different values every run, because the fixed version uses crypto.getRandomValues in the browser and crypto.randomBytes in Node — it does not consult Math.random at all. That one experiment is the whole vulnerability in miniature: weak seed, predictable output, enumerable keyspace.

How to fix it

If you can, stop generating security-sensitive values with crypto-js entirely. Modern JavaScript gives you the platform API for this, with zero dependencies:

// Browser
const bytes = crypto.getRandomValues(new Uint8Array(32));

// Node.js 18+
const { randomBytes } = require('crypto');
const bytes = randomBytes(32);

For BIP39-style seed phrases in a browser context, the Web Crypto API’s crypto.getRandomValues is the documented, audited entropy source, and the reference implementations of bip39 use it. If you must keep crypto-js for the algorithm primitives, at minimum upgrade to 4.2.0 and re-verify that every call site that needs entropy is using the fixed generator — then add a test that pins Math.random and asserts the output still changes, so a regression can never sneak back in.

If you hold a wallet

Coinspect’s checker at illbloom.org accepts a wallet address, not a recovery phrase or private key, and matches it against the exposed dataset. A match means assets tied to that phrase may be at immediate risk. A negative result only means the address is not in the currently published datasets — it is not a clean bill of health.

The safe move if your phrase came from any of the affected apps: generate a new seed on trusted hardware, move the funds, and never look back at the old phrase. And beware of the scammers who follow stories like this one. A real recovery service never needs your seed phrase. If a “rescuer” asks for your words or a signature, they are the attack, not the solution.

The bigger lesson

This is the third weak-randomness wallet disaster since 2023, after Milk Sad and the Trust Wallet extension, and it follows the $89 million Coldcard heist by days. The pattern is always the same: software that looks random, that reads random, that tests random — until someone counts the actual search space and finds the foundation is only a few dozen bits deep.

For developers, the lesson is about entropy hygiene, the same way supply-chain news keeps teaching us about dependency hygiene. When I read about the Keyv npm worm, I checked my lockfiles. When I read about leaked API tokens, I scanned my repos with gitleaks. This week, I grepped every project I maintain for WordArray.random and checked what version of crypto-js actually resolved. I wrote about detecting compromised npm packages and about checking your project after a supply-chain incident, and the same habit applies here: verify what your dependencies actually do, not what their names promise.

The math is unforgiving. A random number generator is either secure against enumeration or it is a liability, and Math.random was never the former. Five wallet apps and $5.7 million later, the reminder is cheap at the price — check your crypto-js version today, and if you are using WordArray.random() for anything that protects a secret, replace it with getRandomValues before the next sweep finds you.

Filed under Tech & Gadgets
Last Update: August 9, 2026 by Felix AlterEgo
0 0 votes
Article Rating
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Newest
Oldest Most Voted