event-stream compromise

In October 2018 I investigated why nodemon was emitting a strange deprecation warning, leading to the eventual discovery of the now infamous event-stream npm package compromise. This post shares a unique perspective of the events that unfolded, along with downloadable forensic evidence that has since been purged from the npm registry.

For context, see:

In the media:

Dates and times mentioned or pictured in this post are Australian Eastern Standard Time (AEST).

Forensic evidence

As the compromised and malicious releases have been unpublished from the npm registry and purged from most mirrors and CDNs, for security research purposes you may download the archive event-stream-compromise-evidence.zip:

event-stream-compromise-evidence
├── event-stream@3.3.6
│   ├── LICENCE
│   ├── examples
│   │   ├── data
│   │   ├── map.js
│   │   ├── pretty.js
│   │   └── split.js
│   ├── index.js
│   ├── package.json
│   ├── readme.markdown
│   └── test
│       ├── connect.asynct.js
│       ├── flatmap.asynct.js
│       ├── helper
│       │   └── index.js
│       ├── merge.asynct.js
│       ├── parse.asynct.js
│       ├── pause.asynct.js
│       ├── pipeline.asynct.js
│       ├── readArray.asynct.js
│       ├── readable.asynct.js
│       ├── replace.asynct.js
│       ├── simple-map.asynct.js
│       ├── spec.asynct.js
│       ├── split.asynct.js
│       ├── stringify.js
│       └── writeArray.asynct.js
├── flatmap-stream@0.1.1
│   ├── LICENSE.txt
│   ├── README.md
│   ├── index.js
│   ├── index.min.js
│   ├── package.json
│   ├── perf-test.js
│   └── test
│       └── data.js
└── flatmap-stream@0.1.2
    ├── LICENSE.txt
    ├── README.md
    ├── index.js
    ├── index.min.js
    ├── package.json
    ├── perf-test.js
    └── test
        └── data.js

Chances are you can source these unpublished package versions yourself by Finder searching the node_modules of a project npm installed in last month or so (but not yet updated) for flatmap-stream.

flatmap-stream@0.1.1 contains the malicious code and is the most interesting to look at.

Delayed response

I first caught wind something was fishy a month ago (October 29, 2018), when I could't work out why nodemon was emitting a crypto deprecation warning in Node.js v11 (remy/nodemon#1442):

[DEP0106] DeprecationWarning: crypto.createDecipher is deprecated.

It was not until November 21, 2018 that Ayrton Sparling figured out it was due to an attack:

@FallingSnow’s @remy/nodemon GitHub comment on November 21, 2018

At which point I put out an urgent tweet:

@jaydenseric’s tweet on November 21, 2018

It got very little attention. The JS community that day was apparently more interested in memes and React hooks shitposts:

@ken_wheeler’s tweet on November 21, 2018

It took far too long for the community to mobilize.

I attempted to report the threat to npm the next day, as the event-stream maintainer didn't seem so inclined (a reliable instinct). At first it was not obvious the right way to go about it; I filed a report at npmjs.com/advisories/report, but after submitting it redirected to a 404 page, I got no confirmation email or anything, and there appeared to be no place to track the status. So I tweeted npm to make sure they got it:

@jaydenseric’s tweet on Nov 22 2018

npm didn’t directly reply; they only took down the malicious packages after the story went viral a week later. Their tweet about the incident (November 28, 2018) claimed they were notified “yesterday morning”:

@npmjs’s tweet on November 28, 2018

I replied that in reality, npm was notified a week earlier:

@jaydenseric’s tweet on November 29, 2018

40 minutes later npm Marketing Vice President @cowperthwait initiated a direct message conversation, asking about what went wrong. He revealed that npm was not monitoring Twitter mentions over the US Thanksgiving holiday weekend. Aparently no one checked the mentions upon returning to work either.

@cowperthwait’s direct message on November 29, 2018

npm then put out a new tweet, dropping the wording about when they were notified:

@npmjs’s tweet on November 29, 2018

The nodemon maintainer first responded 22 hours after the threat was detected:

@jaydenseric’s @remy/nodemon GitHub comment on November 22, 2018

A nodemon version without compromised dependencies was publish relatively soon after. nodemon had a head start; the majority of the JavaScript community continued blissfully unaware for a week before the story went viral.

Reflections

Code on GitHub & npm can differ

It is problematic that package source code on Github, what most people scrutinize, can be totally different to what is published to npm. Hackers are exploiting this to undermine the open source “many eyes” security advantage.

For packages with a repository field, the npm publish command could accept a commit hash argument and the actual packaging could take place on npm servers, pulling the relevant commit. This will have the nice side-effect of ensuring packages can be built reliably from dev dependencies and scripts in environments other than the original author’s.

Security report gatekeeping

  • The reporting form could be more obvious and better tested.
  • Reports via alternative channels such as Twitter should not be ignored.
  • There is always a delay between the initial community report and an official advisory or response, if there is one.

It would be nice if there was a way to audit your projects for unconfirmed community reports of active security threats. There needs to be a clear distinction in the reporting process between security vulnerabilities, which should not be immediately disclosed to the public, and active threats, which should.

Erased history

Instead of removing bad versions of compromised or malicious packages from the npm registry as if they never existed, they should be made only installable with a special flag. This would reduce confusion and allow security researchers to do their thing without having to contact each other asking for copies.