event-stream: The npm Maintainer Handover That Backdoored a Bitcoin Wallet
By Sethu Satheesh · 2 Sept 2026 · 21 min read
Threat Actor: right9ctrl (unidentified) · Target: Copay and BitPay wallet users; the npm ecosystem — event-stream averaged ~2M weekly downloads with 1,500+ dependent packages
Source: blog.npmjs.org
Executive Summary
In late 2018 a person using the handle right9ctrl emailed Dominic Tarr, the author of the npm package event-stream, and offered to take over its maintenance. Tarr agreed. His own explanation, posted publicly three months later, is the shortest and most honest account of how open source supply chain attacks actually begin:
"he emailed me and said he wanted to maintain the module, so I gave it to him. I don't get any thing from maintaining this module, and I don't even use it anymore, and havn't for years."
event-stream averaged around two million downloads a week and had more than 1,500 packages depending on it directly. There was no credential compromise, no phishing, no stolen session token. Publish rights were handed over, voluntarily, to someone who asked politely.
On 9 September 2018 the new maintainer added a dependency called flatmap-stream — a package that plausibly delivered the flatmap functionality users had been requesting on the repository since 2015. On 5 October the attacker published flatmap-stream@0.1.1, identical to its predecessor except for an encrypted payload appended to the end of its minified build output.
The payload's design is the most instructive part of the incident. It decrypted itself using npm_package_description — an environment variable npm sets automatically at install time to the description of the root package being built. On virtually every machine that installed it, that variable held the wrong value, decryption produced garbage, and nothing happened. The correct key was the string A Secure Bitcoin Wallet: the package description of Copay, the open-source bitcoin wallet published by BitPay.
The malicious code therefore lay inert across millions of installations while remaining functionally invisible to anyone who ran it, analysed it dynamically, or read its source without knowing the key. It activated only inside Copay's release build, injected a further stage into a Copay dependency so the code would run inside the shipped application, and then exfiltrated private keys — but only from wallets holding more than 100 BTC or 1,000 BCH.
It was found on 26 November 2018, roughly two and a half months after introduction, and only because a chain of curiosity beginning with an unrelated Node.js deprecation warning led a user to look at what flatmap-stream actually contained.
Copay versions 5.0.2 through 5.1.0 shipped the backdoor. BitPay released 5.2.0 and told users to move funds to a new wallet rather than restore from a seed phrase. To this day, no public figure exists for how much was taken. BitPay's engineers said they were still investigating whether it was ever exploited, and no follow-up was published. right9ctrl was never identified; both accounts were removed. No CVE was assigned, no government advisory was issued, and no charges were brought.
Verification of Claims
Claim: The original maintainer voluntarily transferred publish rights to an unknown person who asked by email.
→ Verified — Stated by Dominic Tarr himself in issue #116 on 22 November 2018: "he emailed me and said he wanted to maintain the module, so I gave it to him." npm's own incident post describes it as a social engineering attack in which the attacker "posing as a maintainer, took over maintainership of the event-stream module."
Claim: The malicious dependency flatmap-stream was added to event-stream on 9 September 2018 in version 3.3.6.
→ Verified — Confirmed by npm's incident post and independently by Snyk and academic analysis of the commit history.
Claim: The payload was decrypted using the root package's description as an AES-256 key, and only executed for Copay.
→ Verified — Documented by Snyk's post-mortem and corroborated by independent analyses. The key was drawn from npm_package_description, which npm sets at install time, and the targeting string was Copay's description, "A Secure Bitcoin Wallet."
Claim: The payload triggered only for wallets holding more than 100 BTC or 1,000 BCH. → Verified — Described in Snyk's analysis and confirmed in the BitPay issue thread by a participant reviewing the decrypted code: "only triggering the upload of the private keys for wallets that had genuinely over 100 BTC in there."
Claim: Copay versions 5.0.2 through 5.1.0 shipped the malicious code, and the BitPay app was not affected. → Verified — Stated directly by BitPay in bitpay/wallet issue #9346 on 26 November 2018: "we have only confirmed that the malicious code was deployed on versions 5.0.2 through 5.1.0 of our Copay and BitPay apps. However, the BitPay app was not vulnerable to the malicious code."
Claim: Bitcoin was stolen from Copay users. → Unverified — BitPay stated on 26 November 2018 that it was "still investigating whether this code vulnerability was ever exploited against Copay users," and no subsequent public confirmation, loss figure, or victim count was published by BitPay. Academic analysis of the incident states the attack "succeeded, directly affecting several users," without disclosing counts or amounts. The theft capability is not in doubt; confirmed losses were never quantified publicly.
Claim: The malicious package was downloaded 8 million times.
→ Unverified — The figure describes downloads of event-stream across the roughly two and a half months the dependency was present, not downloads of the weaponised flatmap-stream@0.1.1, and certainly not installations where the payload decrypted successfully. Given that decryption required Copay's package description as the key, the population where the code did anything at all is vastly smaller than the download count and was never published.
Claim: A CVE was assigned, a government advisory was issued, or the issue appears in CISA's Known Exploited Vulnerabilities catalog.
→ False — No CVE exists for flatmap-stream or event-stream in the National Vulnerability Database. No CISA or US-CERT advisory was published for this incident. No related entry appears in the KEV catalog. Ecosystem-level advisories exist from npm, Snyk and GitLab, but no government body issued guidance.
Claim: The attacker was identified.
→ False — The right9ctrl GitHub and npm accounts were removed and the person behind them has never been publicly identified. No arrest, charge or attribution to a named individual or group has been reported in the years since.
Timeline
| Date | Actor | Event | Source |
|---|---|---|---|
| 2015-07-31 | Community | Flatmap functionality discussed on the event-stream repository — the feature request later used as pretext | Academic analysis |
| 2018-08-05 → 09-04 | right9ctrl | Contacts Dominic Tarr by email offering to maintain the module | Tarr / academic analysis |
| 2018 (Aug–Sep) | Dominic Tarr | Grants repository access and npm publish rights | Tarr, issue #116 |
| 2018-09-09 | right9ctrl | Adds flatmap-stream as a dependency of event-stream@3.3.6 (commit e3163361) |
npm |
| 2018-10-05 | right9ctrl | Publishes flatmap-stream@0.1.1 carrying the encrypted payload in its minified build output |
Snyk |
| 2018-10-29 | Community | A deprecation warning concerning EVP_BytesToKey draws attention to the package's use of crypto |
Snyk / academic analysis |
| 2018-11-20 | FallingSnow | Opens issue #116, "I don't know what to say.", on the event-stream repository | GitHub |
| 2018-11-22 06:35 UTC | Dominic Tarr | Publicly explains the handover: "he emailed me and said he wanted to maintain the module, so I gave it to him" | GitHub |
| 2018-11-26 | npm Security | Discovers and confirms the malicious package; removes flatmap-stream and takes ownership of event-stream |
npm |
| 2018-11-26 18:36 UTC | Community | Opens bitpay/wallet issue #9346 | GitHub |
| 2018-11-26 18:39 UTC | Community | Publishes exfiltration infrastructure details: copayapi.host, associated IPs and registrant email |
GitHub |
| 2018-11-26 22:36 UTC | BitPay | Confirms Copay 5.0.2–5.1.0 affected; releases 5.2.0; instructs users to move funds using Send Max | GitHub |
| 2018-11-27 | Snyk | Publishes technical post-mortem | Snyk |
Attack Anatomy
Initial Access — Asking
There was no intrusion. The attacker sent an email offering to help maintain a package, and the maintainer agreed.
This is worth stating plainly because it is the least technical initial access vector in any incident this publication has covered, and the hardest to defend against. event-stream was widely depended upon, unpaid, and maintained by someone who had stopped using it years earlier. Every condition that made the handover reasonable — an unmaintained but popular package, an author with no remaining interest, a volunteer offering to help — is a normal and generally desirable state of affairs in open source.
The academic reconstruction of the incident notes the attacker's pretext was drawn from the repository's own history: flatmap functionality had been requested publicly as far back as 2015 and the maintainer had been receptive. The offer to help therefore arrived pre-justified, aligned with a feature the project already wanted.
Technique mapping (this paper's assessment): T1195.001 Compromise Software Dependencies and Development Tools; T1199 Trusted Relationship.
Establishing Trust — Legitimate Contributions First
The attacker did not immediately introduce malicious code. Contributions were made, the package was maintained, and the new dependency flatmap-stream — which delivered the promised functionality — was added on 9 September 2018 in a commit that reads, in isolation, as ordinary maintenance.
The commit that introduced it is the one a reviewer would have had to catch. As a participant in the BitPay thread observed at the time, the change looked innocuous from a raw code perspective; what should have prompted scrutiny was the structural fact of a new transitive dependency appearing in a widely used package, from a maintainer who had only recently arrived.
Weaponisation — Publishing the Payload Separately
The malicious code was never committed to event-stream. It was published on 5 October 2018 in flatmap-stream@0.1.1, and only in the package published to npm — appended to the end of the minified build output rather than present in readable source.
The separation matters. Anyone auditing event-stream would find a clean repository and a new dependency. Anyone auditing flatmap-stream's source repository would find nothing either. The payload existed only in the built artefact distributed by the registry, which is a distinct thing from the code the project publishes, and which almost nobody reads.
T1027 Obfuscated Files or Information; T1027.009 Embedded Payloads.
The Key — Targeting by Decryption Failure
The most elegant element of the attack is also the reason it survived two and a half months of mass distribution.
When npm installs a package it exports environment variables describing the root project, among them npm_package_description. The payload used the value of that variable as an AES-256 key to decrypt a blob stored in test/data.js — a path chosen because a file of opaque bytes in a test fixtures directory is unremarkable.
On any machine other than the target, decryption produced meaningless output and execution stopped. There was no check to reverse-engineer, no comparison to a hardcoded target string in the clear, no conditional branch an analyst could read. The targeting was the decryption. An analyst examining the package without possessing the key could not determine what it did, because the payload was not merely hidden — it was cryptographically unavailable.
The correct key was A Secure Bitcoin Wallet, the package description of Copay.
This defeats the standard defensive toolchain by construction. Static analysis sees an encrypted blob. Dynamic analysis in a sandbox executes the decryption, gets garbage, and observes no malicious behaviour. The academic analysis of the incident concludes explicitly that conventional static and dynamic techniques would have failed against this selective activation design.
T1480.001 Execution Guardrails: Environmental Keying.
Staging — Reaching Inside the Application
Decrypting successfully was not sufficient to steal anything, because the code ran at build time rather than inside the running wallet. The payload resolved this by injecting a further stage into @zxing/library, a dependency Copay used for QR code scanning, so that the final code would be bundled into the shipped application and execute on users' devices.
The result is a three-stage chain in which each stage exists in a different place: an encrypted blob in a test fixture of a transitive dependency, decrypted by a build-time environment variable, injecting into a different dependency, to run at application runtime on an end user's phone.
T1195.002 Compromise Software Supply Chain; T1554 Compromise Host Software Binary.
Objective — Selective Theft
The runtime payload examined the wallet's balance and acted only where holdings exceeded 100 BTC or 1,000 BCH. Below those thresholds it did nothing.
The threshold is a deliberate detection trade-off. Large wallets are rare, so the number of victims stays small, the volume of exfiltrated traffic stays low, and the probability that any given user notices anomalous behaviour approaches zero — while the value per successful theft, at 2018 prices, ran into millions of dollars.
Where the threshold was met, private keys and wallet details were transmitted by HTTP POST on port 8080 to copayapi.host.
T1005 Data from Local System; T1041 Exfiltration Over C2 Channel.
Discovery — A Deprecation Warning
Nothing in the defensive stack found this. The thread that unravelled it began on 29 October 2018 with a Node.js deprecation warning about EVP_BytesToKey, a legacy key-derivation function — surfacing, incidentally, the fact that a stream utility package was performing cryptography at all.
That oddity prompted questions, questions led to issue #116 on 20 November, and analysis by multiple community members over the following week established what the package did. npm's security team confirmed and removed it on 26 November.
The gap between the first anomaly and public understanding was nearly a month, and the gap between introduction and discovery was two and a half. Both intervals were spent in plain sight, in a public repository, in one of the most heavily trafficked packages in the ecosystem.
Threat Actor Profile
right9ctrl has never been identified.
Both the GitHub and npm accounts were removed after the incident. The email address associated with the GitHub account, right9ctrl@outlook.com, and the registrant email for the exfiltration domain, kvlguuvh@sharklasers.com — a disposable address from a temporary mail provider — are the only identifiers ever surfaced publicly, and neither leads anywhere.
The exfiltration infrastructure was rented from three unrelated providers: OVH in France, LibertyVPS, and Shinjiru, a Malaysian host that markets offshore privacy. None of this constitutes attribution.
What the operation demonstrates is patience and precision rather than technical novelty in the exploit sense:
The social engineering was the sophisticated part. Identifying an unmaintained but heavily depended-upon package, finding a feature request its maintainer had endorsed, offering to implement it, and waiting weeks before weaponising is a plan, not an opportunistic grab.
The environmental keying shows an understanding of how defenders work. Choosing a decryption key that exists only in the target's build environment is a deliberate answer to the question "what happens when someone analyses this?"
The threshold logic shows an understanding of economics. Restricting theft to wallets above 100 BTC maximises value per victim while minimising the number of people in a position to notice.
No commercial threat intelligence vendor has published an attribution. No government has issued one. No charges have been brought. This paper records no attribution rather than inferring one from tradecraft.
Technical Indicators
Defanged. Sourced from community analysis published in bitpay/wallet issue #9346 on 26 November 2018 and from vendor post-mortems.
Exfiltration
copayapi[.]host private key exfiltration endpoint, HTTP POST port 8080
51.38.112[.]212 resolved at time of disclosure (OVH)
145.249.104[.]239 previously resolved (LibertyVPS)
111.90.151[.]134 associated (Shinjiru, Malaysia)
Packages
flatmap-stream@0.1.1 weaponised release, 5 October 2018
event-stream@3.3.6 introduced the dependency, 9 September 2018
@zxing/library injected at build time to reach application runtime
Copay 5.0.2 – 5.1.0 shipped the backdoor; fixed in 5.2.0
Accounts and registration
right9ctrl GitHub and npm accounts (both removed)
right9ctrl@outlook[.]com GitHub account email
kvlguuvh@sharklasers[.]com copayapi[.]host registrant (disposable provider)
e3163361 commit adding the malicious dependency
Detection guidance
Payload location end of the minified build output of flatmap-stream
Encrypted stage test/data.js, disguised as a test fixture
Decryption key npm_package_description environment variable
Target string "A Secure Bitcoin Wallet" (Copay's package description)
Trigger condition wallet balance > 100 BTC or > 1,000 BCH
Signature of exposure HTTP POST traffic on port 8080 to copayapi[.]host
Dependency check npm ls event-stream flatmap-stream
Legal and Regulatory Response
Government directives. None. No CISA or US-CERT advisory was published for this incident. No CVE was assigned — a search of the National Vulnerability Database for flatmap-stream returns no results — and no related entry appears in CISA's Known Exploited Vulnerabilities catalog. The incident produced no government guidance of any kind.
Law enforcement. No arrest, charge or indictment has been publicly reported. No agency has confirmed an investigation.
Sanctions. None.
Ecosystem response. npm Security removed flatmap-stream from the registry, took ownership of event-stream to prevent further abuse, and published an account of the incident. Snyk and GitLab added advisory entries to their vulnerability databases. These are the mechanisms that actually functioned, and they are operated by private companies and a package registry rather than by any public authority.
Vendor response. BitPay released Copay 5.2.0 and instructed users of affected versions not to open the app, to move funds to a new wallet using the Send Max feature, and specifically not to restore using the twelve-word recovery phrase — recovery would have re-imported keys that must be assumed compromised. BitPay stated it was still investigating whether the vulnerability had been exploited.
The regulatory record here is empty, and the emptiness is the finding. An attack that placed key-stealing code in one of the most widely installed packages in the JavaScript ecosystem, and that reached a production cryptocurrency wallet, generated no CVE, no advisory, no directive and no prosecution. What contained it was a package registry's security team, a handful of curious users in a public issue thread, and one vendor's own disclosure.
Impact Assessment
Confirmed theft was never quantified. BitPay said it was investigating and published no conclusion. Academic analysis states that the attack succeeded and directly affected several users, without numbers. Given that the payload targeted wallets above 100 BTC — worth roughly four million dollars at late-2018 prices — the potential per-victim loss was severe, but the public record contains no total.
The exposure population is far smaller than the download count, and far larger than the victim count. Around eight million downloads of event-stream occurred while the dependency was present. Almost all of those decrypted nothing. The genuinely exposed population is users of Copay 5.0.2–5.1.0, and of those only holders above the threshold were targeted. Conflating these three numbers — as much of the coverage did — overstates the harm while obscuring how precisely it was aimed.
Recovery was unusually costly for affected users. Because private keys must be assumed compromised, the remediation was not "update the app" but "move every coin to a new wallet, and do not restore from your seed phrase." That is an irreversible, fee-incurring, high-stakes operation, and for a self-custody wallet there is no mechanism to reverse a theft that already happened.
The maintainer bore reputational consequences for a structural problem. Dominic Tarr received substantial public criticism for handing over the package. His position — that he had derived nothing from maintaining it, no longer used it, and had not for years — describes the actual economics of most widely depended-upon open source. event-stream had two million weekly downloads and no funding, no security review process, and no succession plan. The incident is frequently framed as a maintainer's lapse of judgment; it is better read as the predictable outcome of an ecosystem that consumes volunteer labour at industrial scale and provides nothing in return.
Ecosystem effects were durable. The incident is a standard reference point in subsequent discussion of dependency risk, and it materially shaped later work on package provenance, lockfile discipline, and registry-side detection. The specific pattern — social-engineer a maintainer handover, wait, publish the payload only to the registry — has recurred repeatedly since.
Lessons and Defensive Recommendations
For Security Teams and SOC Analysts
Treat a change of maintainer on a dependency as a security-relevant event. It is observable — registry metadata records publisher changes — and it is the single strongest signal available for this attack class.
Alert on new transitive dependencies entering the tree, not only on direct dependency changes. flatmap-stream never appeared in anyone's package.json; it arrived beneath event-stream.
Do not treat clean dynamic analysis as evidence of safety for packages that perform decryption at install or build time. Environmental keying means a sandbox will produce a clean result by design. The presence of crypto in a package with no reason to need it is itself the signal — that is precisely what the EVP_BytesToKey deprecation warning exposed.
Watch for high-entropy blobs appearing in dependency updates, particularly inside test fixtures and minified build output. A future attacker can split the blob to defeat naive entropy checks, but the technique still raises the cost.
For Developers and Platform Engineers
Lockfiles are a security control, not a convenience. A committed lockfile with integrity hashes means a republished package does not silently enter your build. This attack depended on version ranges resolving to a newly published dependency without review.
Compare the published package against the source repository. The payload existed only in the npm artefact, never in git. Any process that reviews the repository and installs from the registry is reviewing a different artefact from the one it runs.
Minimise build-time environment exposure. The decryption key was an environment variable npm sets automatically. Build environments hand a great deal of context to every install script that runs in them.
Prefer --ignore-scripts in CI where feasible, with an explicit allow-list for the packages that genuinely need lifecycle scripts.
Audit dependency trees for packages with a single maintainer, no recent releases, and high dependent counts. That profile — indispensable and unmaintained — is what this attacker searched for, and it is enumerable.
For Registries and Ecosystem Maintainers
Surface maintainer changes prominently to consumers. A publisher change on a package with two million weekly downloads should be visible to everyone who depends on it, not discoverable only by inspection.
Publish provenance linking a registry artefact to the commit and build that produced it. This attack turns on the divergence between repository and artefact, and provenance attestation closes exactly that gap.
Treat the addition of a new dependency by a recently added maintainer as a review trigger. The signal was available in the metadata for two and a half months.
For Leadership and CISOs
Your dependency tree includes volunteers who owe you nothing. The maintainer of event-stream was under no obligation to your organisation, was paid nothing by anyone, and made a decision that was entirely reasonable from where he stood. Any risk model that assumes diligence proportional to a package's importance is describing a world that does not exist.
Funding critical dependencies is a security control, not philanthropy. The specific condition this attack exploited — an unpaid maintainer with no remaining interest in a package thousands of companies depend on — is addressable with money, and is not addressable with policy.
Note again what did not happen. No regulator acted, no advisory was issued, no CVE was assigned, nobody was charged. Detection came from a deprecation warning noticed by a stranger. Any strategy that relies on external notification of supply chain compromise should be calibrated against that.
Sources
- npm — Details about the event-stream incident
- dominictarr/event-stream issue #116 — "I don't know what to say."
- bitpay/wallet issue #9346 — event-stream dependency attack steals wallets from users of copay
- Snyk — A post-mortem of the malicious event-stream backdoor
- Snyk — Malicious code found in npm package event-stream downloaded 8 million times
- A Systematic Analysis of the Event-Stream Incident
- LWN.net — event-stream, npm, and trust
- Thomas Hunter II — Compromised npm Package: event-stream
- GitLab Advisory Database — flatmap-stream GMS-2018-46
- GitLab Advisory Database — event-stream GMS-2018-45
- Cointelegraph — BitPay's Copay Wallet Compromised by Malicious Code
- The Hacker News — Rogue Developer Infects Widely Used NodeJS Module to Steal Bitcoins
- NVD — keyword search for flatmap-stream (no results)
- CISA — Known Exploited Vulnerabilities Catalog
Related Research
Excerpt For 33 hours an attacker rerouted the addresses Softaculous updates come from, obtained a genuine TLS certificate for the diverted domains, and served a malicious Virtualizor update that nothing in the chain was able to reject.
Nobody was hacked. A Chinese CDN company bought the polyfill.io domain, and every site that had ever pasted the script tag started serving whatever the new owner wanted. Four months later it was redirecting phones to betting scams.
A sophisticated multi-year supply chain attack compromised the widely used xz compression library, embedding a backdoor in liblzma that targeted OpenSSH authentication on systemd-based Linux distributions. The attacker, operating under the persona 'Jia Tan,' spent over two years building trust as a contributor before gaining maintainership and inserting the malicious code.