Safeguard
Software Supply Chain Security

event-stream npm package backdoor incident

How a routine maintainer handoff let attackers slip a Bitcoin-stealing backdoor into event-stream, hitting millions of npm installs for ten weeks.

Nayan Dey
Security Researcher
7 min read

In September 2018, someone calling themselves "right9ctrl" asked Dominic Tarr, the original author of the npm package event-stream, for publish access so they could help maintain it. Tarr, who had built the utility back in 2011 and moved on to other projects, said yes. Within days, the new maintainer shipped event-stream 3.3.6 with a new dependency called flatmap-stream. That dependency contained an encrypted payload built to steal Bitcoin private keys from a specific cryptocurrency wallet app. Because event-stream was pulled by close to 2 million npm installs a week, the backdoor rode into an enormous number of production builds before anyone noticed. It sat live for roughly ten weeks before a developer flagged the extra dependency as suspicious. The event-stream incident became one of the first widely cited examples of an npm supply chain attack executed through legitimate maintainer transfer rather than a compromised account, and it still shapes how security teams think about transitive dependency risk today.

What was the event-stream npm backdoor?

The event-stream npm backdoor was malicious code smuggled into a popular JavaScript utility library through a new, attacker-controlled sub-dependency rather than a direct edit to event-stream itself. Event-stream was a widely used stream-processing helper — a small utility for chaining and transforming Node.js streams — with no history of security problems in its first seven years. In version 3.3.6, published September 8, 2018, the new maintainer added a single line requiring a package called flatmap-stream, version 0.1.1. Flatmap-stream looked innocuous in the registry: a tiny module with a handful of files. Buried inside it was an AES-256-encrypted block of code that only activated under specific conditions, which is why it evaded casual review for so long. Because event-stream itself was untouched and readable, and the actual payload lived one dependency layer down, most developers who skimmed event-stream's own source saw nothing alarming.

How did the attacker get control of event-stream?

The attacker got control the easy way: they simply asked for it, and the burned-out original maintainer said yes. Dominic Tarr had written event-stream in 2011 and, by his own account, had largely stopped actively developing it years before 2018 while it continued to accumulate millions of weekly downloads across the ecosystem. When "right9ctrl" emailed offering to take over maintenance duties, Tarr granted npm publish rights, a routine and, at the time, completely unverified handoff. There was no code review gate, no second maintainer sign-off, and no npm-side vetting of the new account before it gained the ability to push new versions to millions of downstream consumers. This is the detail that made the case so influential: no credentials were phished, no CI pipeline was breached. The compromise happened entirely through the normal, trusted social process by which open source maintainership changes hands.

How did the malicious code actually work?

The malicious code worked by hiding a targeted, encrypted payload inside flatmap-stream and only decrypting it in the presence of one specific application. The decryption key wasn't hardcoded — it was derived from the description field inside the package.json of whatever application had installed it. In practice, that meant the payload would only unlock and run inside a single downstream consumer: the Copay and BitPay Bitcoin wallet apps, built by BitPay, which depended on event-stream indirectly through the bitcore-wallet-client library. Everywhere else it installed, flatmap-stream was functionally dead weight, which is exactly why it passed unnoticed in the tens of thousands of other projects that pulled it in as a transitive dependency. Once decrypted inside a Copay build, the code searched the wallet application for private keys and wallet seeds, and specifically targeted wallets holding between roughly $100 and $1,000,000 in cryptocurrency — a range apparently chosen to skip trivial wallets while avoiding the kind of attention a theft from an exchange-scale wallet would draw.

Who was targeted, and what happened to Copay users?

The target was BitPay's Copay wallet, and the practical impact was that BitPay told affected users to move their funds to new wallets rather than trust that their existing keys were still secret. Copay shipped versions 5.0.2 through 5.1.0 with the compromised event-stream chain bundled in, covering both the desktop Electron app and, in some builds, the mobile app. BitPay's own incident writeup, published shortly after discovery, could not rule out that private keys from installs during that window had been exfiltrated to the attacker's server, so its guidance was blunt: treat any wallet created or opened with those versions as burned, and sweep the funds into a fresh wallet with newly generated keys. Because Copay was a consumer-facing Bitcoin wallet rather than internal tooling, this was one of the earliest npm supply chain attacks with a direct, quantifiable path from a compromised dependency to stolen end-user cryptocurrency, not just a theoretical CI/CD risk.

How was the backdoor discovered and fixed?

The backdoor was discovered by a developer who noticed flatmap-stream had no clear purpose and raised it as a GitHub issue on the event-stream repository on November 20, 2018 — about ten weeks after the malicious version first shipped. Once the report surfaced, the response moved quickly: npm's security team pulled flatmap-stream from the registry, and Dominic Tarr published event-stream 4.0.0 within days, stripping the flatmap-stream dependency out entirely and reasserting control over the package. GitHub and npm both issued advisories flagging every version of event-stream between 3.3.6 and 3.3.7 as compromised, and downstream scanners began flagging any lockfile that still pinned flatmap-stream@0.1.1. The roughly ten-week gap between publication and detection is the number that security teams still cite: it's the window during which an ordinary npm install could silently pull in code built to steal cryptographic keys, with nothing in event-stream's own changelog or README hinting at the change.

What does the event-stream incident teach about npm supply chain risk?

The core lesson is that a dependency doesn't have to be popular, actively exploited, or even directly imported to be dangerous — event-stream's own code never changed, only what it pulled in did. Most organizations audit the packages they add to package.json far more carefully than the transitive dependencies those packages bring along, yet flatmap-stream reached victims exactly that way, two hops removed from any line a developer had actually reviewed. It also showed that maintainer identity is itself an attack surface: npm had no mechanism in 2018 to flag a sudden, unvetted transfer of publish rights on a package with millions of weekly installs, and many registries still lean heavily on informal trust for exactly this kind of handoff. Finally, the targeted, encrypted-until-matched payload design demonstrated that malicious code can be built to stay invisible to anyone who isn't the intended victim, which means "nothing bad happened when I tested it" is not evidence a package is clean.

How Safeguard Helps

Safeguard is built for exactly this failure mode: a clean-looking direct dependency hiding a dangerous transitive one. Reachability analysis traces whether code from a package like flatmap-stream would ever actually execute in your application's call paths, so teams can prioritize the handful of dependencies that matter instead of triaging every CVE in a 40,000-package tree. Griffin AI reviews new and updated package versions — including maintainer and publish-rights changes — for behavioral red flags like unexplained new sub-dependencies or obfuscated payloads before they land in a build. SBOM generation and ingest give teams a queryable record of every transitive package in production, so a repeat of the event-stream case can be answered in seconds instead of weeks. When a risky package is confirmed, Safeguard opens an auto-fix pull request to pin, patch, or remove it, cutting the exposure window from ten weeks down to the length of a CI run.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.