TL;DR
On May 27, 2026, 431 crypto accounts across Bitcoin, Ethereum, Tron, Rootstock, and Polygon were emptied inside a few hours for roughly $3.14 million, with Bitcoin carrying $2.57 million of it. A second, slower window ran from May 30 to July 13 and took another 522 seeds for about $2.55 million. The cause was CryptoJS.lib.WordArray.random(), the function five wallet apps relied on for the entropy behind their users' BIP39 recovery phrases. Coinspect traced the drains back to it while the attackers were still working, and named the research Ill Bloom. The function used a custom Multiply-With-Carry generator seeded from Math.random(), so a nominal 128 bits of entropy collapsed to a search space of about 2^39, enumerable on a normal computer. crypto-js fixed this in version 4.0.0 in February 2020. The keys it had already made stayed broken, because entropy is decided once, at generation, and no upgrade reaches back.
What Is WordArray.random() Doing Wrong?
A BIP39 recovery phrase is a human-readable encoding of a random number. Ask for a 12-word phrase and the wallet requests 128 bits from its randomness source, maps those bits onto a wordlist, and derives every private key you will ever hold from the result. The words carry no strength of their own. All of the security lives in whether that original number was unguessable.
crypto-js supplied that number through CryptoJS.lib.WordArray.random(). The GitHub Security Advisory for CVE-2026-71851 describes the implementation as "a custom variation of George Marsaglia's Multiply-With-Carry PRNG, seeded from Math.random()". Multiply-With-Carry is a decent statistical generator and a terrible cryptographic one, because its entire future output is a deterministic function of its seed, and here the seed came from a runtime Math.random() call carrying nowhere near 128 bits of unpredictable state.
The advisory puts the collapse in exact terms: nominal requests for 128 or 256 bits of entropy produce effective search spaces of approximately 2^39 and 2^47 possibilities. 2^39 is about 550 billion candidates. That is a weekend of commodity hardware, not a heat death of the universe. GitLab's advisory database rates it 9.0 Critical under CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H and files it under three weakness classes at once: CWE-331 insufficient entropy, CWE-334 small space of random values, and CWE-338 use of a cryptographically weak PRNG.
Nothing downstream repairs a phrase once it has been generated from that output. PBKDF2 stretching, a passphrase, hardware storage, none of it adds entropy that was never there. That is why the remediation for every affected user is a new phrase, rather than a new device or a stronger passphrase on the old one.
The Attack, Step by Step
The enumeration is the whole attack, and it runs entirely offline until the final transaction.
| Step | Action | Where it happens |
|---|---|---|
| 1 | Reimplement the vulnerable PRNG and enumerate its feasible seed states | Attacker's own hardware |
| 2 | Convert each candidate output into a valid BIP39 recovery phrase | Offline |
| 3 | Derive private keys and addresses across common derivation paths and networks | Offline |
| 4 | Compare the derived address set against public blockchain data for balances | Public RPC and explorers |
| 5 | Sign and broadcast sweeps from every funded match | On-chain, first and only visible moment |
Coinspect ran steps 1 through 4 defensively and described the same pipeline: reproduce the generation process, enumerate the weak seed space, derive addresses, and compare them against public blockchain data. Its first address set started from 12-word phrases on selected derivation paths, then widened across Bitcoin, Ethereum-style chains, Tron, Rootstock, Solana, Cosmos, and Nano. How much of the space you cover comes down to four choices: entropy sizes, mnemonic languages, derivation paths, and supported networks.
Researchers and attackers were running the same computation with opposite intent. For any individual wallet, the outcome came down to who finished first.
There was no interaction with the victim. No approval to abuse, no signature to phish, no malicious frontend. A wallet that had never connected to anything was still drained, because the theft happened inside the attacker's process until the moment of broadcast.
Why Did a Library Fixed in 2020 Keep Shipping the Bug?
The weakness was reported publicly in issue #7 on the crypto-js repository, opened on March 21, 2014, citing Chromium's own documentation on why Math.random() is unsuitable for cryptography. The vulnerable Multiply-With-Carry implementation then shipped in version 3.1.2-4 in June 2014 (three months after the issue describing the problem was already open).
What follows is worse than neglect. Versions 3.2.0 and 3.2.1 replaced the weak generator with the platform's native secure random. Then, as the Ill Bloom writeup on the library records, 3.3.0 reverted it, because adopting native randomness was deemed a breaking change. A secure random source was shipped, then taken back out because it broke consumers. The revert note called it a breaking change. It broke something either way.
Version 4.0.0, in February 2020, restored platform-native randomness and closed it properly. Anyone tracking the 3.x line kept resolving to a version that made guessable keys, for six more years.
The path from library to wallet is what turns this from a packaging footnote into an incident. Most affected apps never called crypto-js directly. They pulled in ferrumnet/bip39, a fork of the standard BIP39 library that adopted the vulnerable crypto-js specifically for React Native compatibility, and that fork received no updates after November 2019. The wallet team picked a mnemonic library, the mnemonic library picked a crypto library for mobile compatibility, and the crypto library picked backward compatibility over a secure RNG. Nobody in that chain made an obviously reckless call. The CVE title names the shape exactly: insufficient entropy in cryptographic secret generation via a vulnerable dependency chain. Coinspect adds that crypto-js has been effectively unmaintained since 2023.
A lockfile is the control most teams assume covers this. It pins a version and says nothing about the quality of that version's random(), and an audit carries the same boundary, certifying the code it was shown on the day it was shown. Neither one was ever going to raise a hand about a transitive dependency whose last release was in November 2019.
Inside the Attacker's Playbook
Four details separate this from a generic brute-force story.
The sweep was staged, then patient. May 27 was a burst. Hundreds of unrelated wallets sent their balances to the same few collection addresses within hours, which is how the coordination was spotted at all, and one Bitcoin address alone gave up more than $1.1 million. That says the attacker had already finished the offline enumeration and was clearing a precomputed list. The second window, May 30 through July 13, behaves differently: 522 seeds over six weeks reads as a standing watch, sweeping addresses as fresh deposits landed rather than as they were discovered. By July 13 the monitored balances were effectively zero.
The money moved between chains as the campaign matured. Coinspect published a per-chain accounting for the first wave and the second, and the shift is stark.
| Chain | Wave 1 (May 27) | Wave 2 (May 30 to Jul 13) |
|---|---|---|
| Bitcoin | $2,573,522 | $331,420 |
| Ethereum | $285,778 | $26,301 |
| Rootstock | $177,225 | $2,939 |
| Tron | $80,970 | $2,186,066 |
| Polygon | $23,473 | $3,228 |
Wave one was a Bitcoin story. Wave two was 86 percent one Tron account. The same analysis reconstructs the vulnerable population as holding about $12.56 million at its April 2022 peak, so the drains caught a fraction of what had moved through these addresses over the years.
One wallet was watched, flagged, and drained anyway. Coinspect identified roughly $2.1 million in USDT sitting at risk on June 10 and could not reach the owner in time. The Tron account went on July 4 for about 2.18 million USDT, the single largest loss in the whole campaign. Knowing which address is doomed does not help when you have no channel to the person holding it, and self-custody is precisely the design where that channel does not exist. That balance was Tether on Tron, which the issuer is able to freeze. Whether a freeze was ever requested or attempted is not in the public record.
The attacker widened the wordlist, not just the balance sheet. On July 20 and 21, a third sweep took 81 seeds derived from Chinese-language mnemonics for about $38,563, out of 142 such seeds Coinspect had identified. The money is trivial. The signal is not: the Ethereum collector addresses were reused from the earlier waves, tying the campaign together and showing the attacker running the same address-set expansion the researchers were, one language at a time.
The economics were never in question. Roughly 550 billion candidates is a bounded, parallel job with nothing to rate-limit and nothing to alert on, run against a target set that had been sitting in public view since 2014.
How Many Wallets Actually Lost Money?
Sources disagree here, and the difference is not cosmetic.
Several outlets ran the figure as roughly 2,100 wallets robbed. One reported that "more than 2,100 wallet addresses across Bitcoin, Ethereum, Tron, Rootstock, and Polygon have fallen victim to Ill Bloom, with total losses above $5.7 million." That conflates two counts. 2,114 is the size of the monitored set: identified vulnerable addresses with any on-chain activity. The addresses that actually lost funds are the ones inside the drain windows, 431 plus 522, or 953, with a further 81 in the Chinese-mnemonic wave.
The right figure depends on the window. Inside the two drain windows Coinspect measured to July 13, 953 accounts lost about $5,691,000, and that is the total the advisories quote. Add the Chinese-mnemonic wave of July 20 and 21 and the known campaign runs to 1,034 accounts and roughly $5,730,000.
The split inside those counts is lopsided. Coinspect separates one identified wallet cohort from an unlabelled remainder, and in both windows the identified cohort holds most of the accounts, 260 of 431 and then 335 of 522, while holding almost none of the money, $283,100 and then $21,561. The app with the most exposed users was not the app that bled. Nobody has published which of the five lost what.
Treat even that as a floor. The address set only covers the entropy sizes, languages, derivation paths, and networks Coinspect chose to enumerate; anything outside that combination is invisible to the count and equally guessable. GitLab's advisory uses the same careful wording, calling roughly $5 million as of July 13, 2026 a measured lower bound.
The trail also stops early. Coinspect's chain analysis follows the money as far as the shared collector addresses and no further. As of publication no public work traces those balances onward to a mixer, a bridge, or an exchange deposit, and nobody has offered an attribution.
Why This Keeps Happening
Ill Bloom is the third significant version of this failure in three years, and the pattern is stable enough to plan around.
In 2023, Milk Sad, CVE-2023-39910, hit the bx seed command in Libbitcoin Explorer, which used a Mersenne Twister seeded from system time. The name comes from the phrase the tool emits when the clock reads zero. In July 2026, a March 2021 Coldcard firmware change routed seed generation to a software PRNG instead of the device's hardware RNG, and attackers swept 1,367 BTC. We covered that one in detail in the Coldcard entropy failure post.
The vendors, the languages and the decades all differ. The shape does not: entropy that looked fine because its output looked fine. A weak generator produces phrases that pass the checksum and the wordlist validation, and no observable property separates a strong 12-word phrase from a guessable one until somebody spends the compute. Most vulnerabilities produce a symptom once exploited. This one produced none for twelve years, and for the holders caught in the May 27 sweep the first indication was an empty wallet.
The scope boundary deserves stating honestly. Hardware wallets and most mainstream software wallets were not affected, and the risk concentrated in smaller mobile and web wallet apps, some dating to 2018. The five confirmed applications, per The Hacker News, are RRWallet and Milo, both discontinued with no fix coming, Bexo Wallet fixed in 20.1.0, NanChat fixed in 1.3.0 and deployed June 12, and Bitcoin Libre fixed in version 4 back in July 2024. Note the gap in that list: Bexo's fix existed as a version number on August 6 while the corrected builds had not yet been uploaded, so users of a discontinued app have nothing to install and users of a fixed one briefly had nothing either.
What Should Operators and Wallet Teams Do Now?
- Check your addresses against the published set today. Coinspect published a free address checker at illbloom.org. Paste in the public address. A match means the phrase behind it is compromised and the funds need to move to a phrase generated elsewhere.
- Treat a patched RNG as a key-rotation event, never a version bump. If any key material in your system was produced by a stack that later fixed its randomness, upgrading the dependency remediates nothing. Generate new keys, migrate the funds, retire the old ones. This is the most commonly skipped step in this class of incident.
- Trace where your entropy comes from, function by function. Not the library name. The exact call. Wallet seeds, session keys, nonces, and API secrets should all terminate at the operating system CSPRNG (
crypto.getRandomValues,crypto.randomBytes,/dev/urandom) with no wrapper in between. Coinspect's guidance is blunt and correct: a randomness failure should be fatal rather than quietly degrading. - Audit the transitive dependencies of your key generation path, and pin them exactly. No wallet team here chose a broken PRNG. They chose a mnemonic library that chose a crypto library that chose compatibility. Walk the chain to the leaf, and treat a caret range on a crypto library as an open subscription to whatever that maintainer decides is not a breaking change.
- Record generation-time provenance in your key lifecycle. Log which library and version produced every seed, key, and nonce your product ships. When the next advisory of this shape lands, the gap between a scoped two-hour response and a two-week panic is whether you can answer "which keys did that code path touch" from a record instead of from memory.
- Watch for the fan-in signature if you custody at scale. Many unrelated addresses sweeping into a few collector addresses within hours is the on-chain shape of a mass key compromise, and it is detectable live. That helps exchanges and custodians. It is no rescue for an individual holder, whose funds are gone by the time the pattern is legible.
Where our own products sit, honestly: runtime monitoring would not have prevented this. Tripwire watches what happens on-chain, and by the time an Ill Bloom sweep reaches the chain the key was already public to anyone who had done the math. The controls that would have caught it are a review of the key-generation path before shipping and dependency scanning that flags a cryptographic library resolving to a known-weak line, which is audit and Sentinel work. Monitoring earns a narrower place: spotting the sweep fast enough to freeze what is downstream.
Frequently Asked Questions
Which crypto-js versions are affected by CVE-2026-71851? All versions below 4.0.0, except 3.2.0 and 3.2.1, which briefly shipped a secure generator before it was reverted in 3.3.0. Version 4.0.0, released February 2020, uses the platform's native cryptographic API and is the fixed line.
Does updating my wallet app fix a phrase it already generated? No. Entropy is fixed at generation. A recovery phrase produced by a vulnerable version stays guessable everywhere it is used, including after you import it into a hardware wallet. The only remediation is generating a new phrase on a fixed version and moving the funds.
Was my hardware wallet affected? Not unless you imported a phrase originally generated by one of the affected apps. Ill Bloom is a flaw in a JavaScript library that specific mobile and web wallets called during phrase creation.
How do I know if I was exposed? Check the public address at illbloom.org. Coinspect built the checker from the address sets it derived from the weak seed space. A match is conclusive. A non-match is reassuring but not a guarantee, because the published sets do not cover every entropy size, language, derivation path, and network.
Is this the same bug as Milk Sad?
Same class, different software. Milk Sad was a Mersenne Twister seeded from system time in Libbitcoin Explorer. Ill Bloom is a Multiply-With-Carry generator seeded from Math.random() in crypto-js. Both reduce BIP39 phrases to an enumerable set.
Sources / References
- Coinspect: Ill Bloom, Investigating a Wallet Generation Vulnerability During Active Exploitation
- Coinspect: Weak Wallet Seed Generation
- Ill Bloom: The CryptoJS Randomness Vulnerability
- Ill Bloom: chain analysis of the first drain window
- Ill Bloom: chain analysis of the second drain window
- Ill Bloom: Chinese-mnemonic drains
- GitHub Security Advisory GHSA-rg76-677x-56q9 (CVE-2026-71851)
- GitLab Advisory Database: crypto-js CVE-2026-71851
- crypto-js issue #7: insufficient randomness in randomBytes, March 2014
- The Hacker News: CryptoJS Weak RNG Behind $5.7 Million in Drains
- The Hacker News: Attackers Exploit Ill Bloom Vulnerability
- SecurityOnline: CryptoJS Randomness Vulnerability Drains Crypto Wallets
- Milk Sad: CVE-2023-39910 summary
- CoinTurk: coverage using the 2,100 wallet figure



