Skip to content
← Blog

Attack post-mortem

event-stream and flatmap-stream: the 2018 npm attack that targeted a single Bitcoin wallet

3 min read

In November 2018, an npm package called flatmap-stream was found to contain code that targeted a specific cryptocurrency wallet — Copay — and exfiltrated wallet keys when present. flatmap-stream was a transitive dependency added to event-stream, a widely used Node.js streaming library, by a new maintainer who had volunteered to take over the project a few months earlier.

This is the canonical case study for maintainer-handoff supply-chain attacks. It is worth re-reading carefully because the social engineering was as important as the code.

How the attack unfolded

event-stream was created and maintained by a developer who, by 2018, had moved on to other things. The package was widely depended-on but received little active development. A user opened a GitHub issue politely offering to take over maintenance. The original author, with no reason to suspect bad faith, transferred the package's npm publishing rights.

Over the next few months, the new maintainer added flatmap-stream as a dependency, made some legitimate-looking commits, and published a version of event-stream that pulled in flatmap-stream. A few weeks later, a malicious version of flatmap-stream was published, targeted specifically at Copay — a Bitcoin wallet that bundled event-stream in its build.

The malicious code ran only inside the Copay wallet's bundled environment. On any other host, the package behaved normally. This made detection during ordinary development extremely difficult.

What the malicious code did

When the package was loaded inside Copay's specific bundle, it decrypted a payload using a key derived from the bundle's metadata, then attempted to extract private keys for any wallet with a balance over a threshold. Successful extractions were exfiltrated to an attacker-controlled server.

The decryption-on-conditional-execution pattern was novel at the time. It defeated naive scanning: the malicious branch never ran in CI, in tests, or on any developer's machine that was not Copay.

Why this attack mattered

Three things make event-stream historically important.

First, it established maintainer handoff as a real attack vector. The transfer happened in public, on GitHub, with no warning signs. Any popular but inactive package is a candidate for the same approach.

Second, it demonstrated target-conditional payloads. Code that runs only on a specific target environment cannot be caught by generic CI testing or by an analyst running the package in isolation.

Third, it showed how transitive dependencies bypass attention. Copay's developers reviewed event-stream once, when they added it. They did not review flatmap-stream, which arrived weeks later as a transitive change in a patch-version bump.

What standard defenses do

Threat feeds eventually caught it. It took ten weeks from publication to detection.

Lockfiles would have helped — but only at the lockfile-update step. Once a dependency is updated and the lockfile is regenerated, you have committed to whatever code came along. A lockfile is not a substitute for reviewing what you pinned.

Provenance, where available, would identify the publisher. A handoff-and-publish from a new maintainer would be visible in the chain of attestations, but provenance does not, by itself, distinguish a legitimate handoff from a malicious one.

What helps in advance

Two practices change the picture.

Treat transitive dependency additions as code changes that require review. When npm ci would change the resolved tree because a maintainer added a new transitive dependency, that's a reviewable event, not a silent automatic upgrade. Lockfile diffs in code review are how you catch this.

Consider publication age before installing. A package version published an hour ago by a new maintainer is qualitatively different from a version that has been live and observed for a week. A cooling-period gate refuses to be in the first cohort of installs for a brand-new version regardless of who published it.

Takeaway

The event-stream and flatmap-stream story is six years old, but every element of it could happen again tomorrow. Maintainers move on; popular packages stay popular; transitive dependencies arrive without ceremony; conditional payloads defeat casual review. The defenses that hold up are the ones that change what gets installed and when: lockfile discipline, transitive-change review, and a publication-age cooling gate.