Cloud Security Office Hours Banner

event-stream / npm 2018

Step-by-step kill chain mapped to MITRE ATT&CK Cloud, sourced from official post-mortems and primary technical analyses.

September-November 2018 Critical npm

event-stream - "Can I Maintain This?" โ†’ Publish Rights Handed Over โ†’ Encrypted Payload in a New Dependency โ†’ Targeted Theft From One Bitcoin Wallet App

In September 2018 the maintainer of event-stream, an npm package doing roughly two million downloads a week that he had not actively maintained since 2012, transferred publish rights to a volunteer who asked for them. The volunteer, using the handle right9ctrl, added a new dependency called flatmap-stream. Inside it was an encrypted payload that decrypted itself using the package description as the key, then targeted exactly one thing: the build process of Copay, a Bitcoin wallet application. It harvested account data and private keys from wallets holding more than 100 BTC or 1,000 BCH. It was found on November 26, 2018, by a developer investigating a deprecation warning. This is the oldest chain on this page and the template for several of the newest. The handover mechanism reappears in XZ Utils six years later, and the maintainer's reply to the outrage - that he had given away a package he no longer used, for free, and owed nobody anything - remains the sharpest statement of the sustainability problem underneath all of it.

~2 million weekly downloads at the time
Handed over on request - no compromise, no theft
Precision-targeted: one wallet app, wallets over 100 BTC
~11 weeks from injection to discovery
๐Ÿ“„ npm - Details about the event-stream incident โ†— ๐Ÿ“„ GitHub issue #116 - "I don't know what to say." โ†—
๐Ÿค Initial Access - Asking Politely
01
The original maintainer, who had not used the package since 2012, transferred publish rights to a volunteer who offered to take it over
T1656 - Impersonation T1195.002 - Compromise Software Supply Chain

No credential was stolen and no system was exploited. Dominic Tarr had written event-stream years earlier, stopped using it in 2012, and continued receiving issues and requests for a package he had no personal stake in. Someone offered to help, and he did the thing the open-source ecosystem asks maintainers to do in that situation: he handed it over. The package still served around two million downloads a week, so the handover transferred write access to a substantial share of the JavaScript ecosystem on the strength of a friendly request. What makes this the template rather than a curiosity is that the same route was taken again, far more patiently, against XZ Utils in 2024 - and in that case sock puppets manufactured the pressure that made handover feel necessary.

Package: event-stream, roughly 2 million downloads per week
Maintainer's position: Had not used it since 2012; unpaid, still fielding issues
Mechanism: Publish rights transferred on request to right9ctrl
Compromise involved: None
Repeated in: XZ Utils, 2024 - same route, with manufactured pressure added
Maintainer Handoverright9ctrlSocial EngineeringT1195.002
๐Ÿ“ฆ Supply Chain - A New Dependency Nobody Questioned
02
Version 3.3.6, published September 9, 2018, added flatmap-stream as a dependency - a package created for this purpose
T1195.001 - Compromise Software Dependencies

The malicious code was never in event-stream itself. A new dependency was added, and that dependency carried the payload - which means anyone auditing event-stream's own source found nothing wrong, and the change that mattered was a single line in a manifest. Adding a dependency is among the most routine things a maintainer does and among the least scrutinized by consumers, most of whom never saw the change at all because it arrived transitively, several hops down a tree they had no reason to inspect. Legitimate later versions were also released, which further normalized the package's activity.

September 9, 2018: event-stream 3.3.6 adds flatmap-stream as a dependency
Where the payload lived: In the new dependency, not in event-stream
Visible change: One line in a manifest
Consumer visibility: Effectively none - it arrived transitively
Auditing event-stream itself: Would have found nothing
flatmap-streamTransitive DependencyManifest ChangeT1195.001
๐Ÿ” Defense Evasion - Encrypted, With the Key in Plain Sight
03
The payload read encrypted data from a file disguised as a test fixture and decrypted it using the package description as the key, so the malicious logic was never readable in the source
T1027 - Obfuscated Files or Information T1059.007 - JavaScript

The construction is elegant in a way that is worth appreciating for what it defeats. The malicious logic sat encrypted in a file dressed as a test fixture, and the decryption key was the description field of the package that would consume it - so the payload only decrypted successfully inside the intended victim's build, and anyone examining the code elsewhere saw an opaque blob that decrypted to nothing. That is targeting enforced by cryptography rather than by a conditional statement, and it makes analysis outside the target environment close to useless. Note how directly this prefigures XZ Utils, where the payload also hid in files presenting as binary test fixtures.

Storage: Encrypted data in a file disguised as a test fixture
Decryption key: The description field of the consuming package
Effect: The payload only decrypts inside the intended victim's build
Analysis elsewhere: An opaque blob that decrypts to nothing
Same idea in 2024: XZ Utils hid its payload in binary test fixtures
Encrypted PayloadTest Fixture DisguiseTargeted DecryptionT1027
โ‚ฟ Impact - Two Million Downloads to Steal From One Application
04
The payload activated only in Copay's build, harvesting account details and private keys from wallets holding more than 100 BTC or 1,000 BCH
T1657 - Financial Theft T1555 - Credentials from Password Stores

Millions of installations, and the attacker wanted exactly one application. The code injected itself into Copay's build, checked balances, and exfiltrated account data and private keys only from wallets above roughly 100 BTC or 1,000 BCH - deliberately ignoring smaller holdings to reduce the chance of anyone noticing. Copay confirmed the malicious code shipped in versions 5.0.2 through 5.1.0. The strategic point is uncomfortable: precision targeting means the enormous population of collateral installs is not a bug in the attacker's plan but the camouflage for it, and it means "we use that package but we're not the target" is true right up until someone else's payload has a broader trigger.

Target: Copay, a Bitcoin wallet application, and its build process
Threshold: Wallets holding more than ~100 BTC or ~1,000 BCH
Taken: Account data and private keys, sent to an external server
Affected releases: Copay 5.0.2 through 5.1.0
Why ignore small wallets: Fewer victims noticing means longer dwell time
CopayPrecision TargetingPrivate Key TheftT1657
๐Ÿ” Discovery - Someone Investigated a Deprecation Warning
05
A developer noticed an unfamiliar dependency while looking into a deprecation notice, opened GitHub issue #116, and the encrypted payload unravelled from there

Eleven weeks passed between injection and discovery, and it ended because one person was curious about something minor. The thread that followed, titled "I don't know what to say.", became one of the more consequential documents in open-source security - not for the technical analysis but for Tarr's response to the anger directed at him. He pointed out that he had published the package for free, stopped using it years earlier, and was being blamed by people who had taken his work without payment and without contributing, for failing to keep guarding it indefinitely. That argument has not been answered since. It is the same condition XZ Utils exploited in 2024, and the reason maintainer funding shows up as a security control on this site rather than as a goodwill gesture.

Injected: September 9, 2018 ยท Discovered: November 26, 2018
Trigger for discovery: A developer investigating a deprecation warning
Venue: GitHub issue #116, "I don't know what to say."
The lasting argument: Free work, abandoned years earlier, guarded indefinitely by whom?
Same condition exploited in: XZ Utils, 2024
Issue #11611 WeeksOSS Sustainability
๐Ÿ›  Response - npm Took the Package Back
06
npm removed the malicious package and version and assumed ownership of event-stream, since the original maintainer no longer had rights to fix it

There is a detail here that catches people out. When Tarr was told about the backdoor, he could not remove it - he had transferred publish rights, so he had no more authority over the package than any other user, and had to ask npm to intervene. npm removed flatmap-stream and the affected version and took ownership of event-stream itself, advising users to run npm audit and update. That is worth holding onto: transferring maintainership is irreversible from the transferring side, so the decision to hand a package over is effectively a decision to trust the recipient permanently, made at a moment when the maintainer is usually tired and relieved that someone volunteered.

Removed: flatmap-stream and the affected event-stream version
Ownership: npm assumed ownership of event-stream
Why the original maintainer could not act: He had already transferred publish rights
Guidance: Run npm audit and update
Structural point: Handover is irreversible from the transferring side
Registry InterventionIrreversible Handovernpm audit

๐Ÿ›ก How to Defend Against This Chain

Pin dependencies by hash and treat a new transitive dependency as a reviewable change. The malicious code arrived as one added line in a manifest, several hops down the tree. A lockfile with enforced integrity hashes means nothing enters your build unreviewed, and tooling that surfaces new transitive dependencies in a diff turns the change that mattered here into something a human actually sees.
Watch for maintainership and ownership changes in your dependency path. This is the earliest instance of the pattern and XZ Utils is the most sophisticated. A package changing hands is a security-relevant event, it is usually visible in public metadata, and for the dependencies in your critical path it is worth alerting on rather than discovering afterwards.
Disable install scripts and constrain what a build can reach. The payload executed during Copay's build and exfiltrated to an external server. Running installs with --ignore-scripts, and default-denying egress from build environments with a narrow allow-list, breaks both halves of that without requiring anyone to have spotted the dependency.
If you handle high-value assets, assume you are the specific target. The payload ignored every install except one application, and within that only wallets above a threshold. Organizations holding cryptocurrency, payment flows or similar should treat their build pipeline as directly targeted infrastructure - reproducible builds, dependency review by a human, and a hard separation between build systems and anything holding keys.
Fund the maintainers, and understand why that is a control and not a courtesy. Tarr's argument was never really rebutted: he gave away free work, stopped needing it, and handed it on when someone offered. Every organization depending on that package benefited from years of unpaid labour and contributed nothing to the moment it became a liability. Funding the projects in your dependency path, and contributing maintenance rather than only issues, is the control that addresses the cause of both this chain and XZ Utils.
Reduce the tree, since every package is a trust decision that outlives its author's interest. event-stream was two million downloads a week of code its author had not needed since 2012. Periodically audit whether each dependency still earns its place, prefer the standard library or a small vendored function over a micro-package, and remember that removing a dependency is the only mitigation that is permanent.

Related defense topics