Compromised npm Package: event-stream

Ownership of a popular npm package, event-stream, was transferred by the original author to a malicious user, right9ctrl. This package receives over 1.5mm weekly downloads and is depended on by nearly 1,600 other packages. The malicious user was able to gain the trust of the original author by making a series of meaningful contributions to the package. The first publish of this package by the malicious user occurred on September 4th, 2018.

The malicious user modified event-stream to then depend on a malicious package, flatmap-stream. This package was specifically crafted for the purposes of this attack. That package contains a fairly simple index.js file, as well as a minified index.min.js file. The two files on GitHub appear innocent enough. However, in the published npm package, the minified version of the file has additional code injected into it. There is no requirement that code being uploaded in an npm module is equivalent to the code stored publicly in a git repository.

The addition of the malicious package to the list of event-stream dependencies came to light on November 20th and is documented heavily in dominictarr/event-stream#116. This issue was made over two months after the compromised package was published. One of the many benefits of open source software is that code can be audited by many different developers. However, this isn’t a silver bullet. An example of this is OpenSSL, which is an open source project receiving some of the highest scrutiny but is still affected by serious vulnerabilities such as Heartbleed.

What does the package do?

The package represents a highly targeted attack. It ultimately affects an open source app called bitpay/copay. According to their README, Copay is a secure bitcoin wallet platform for both desktop and mobile devices. We know the malicious package specifically targets that application because the obfuscated code reads the description field from a project’s package.json file, then uses that description to decode an AES256 encrypted payload.

For projects other than copay, the description field won’t properly match the key used for encryption, and the operation fails silently. The description field for bitpay/copay, which is A Secure Bitcoin Wallet, is the key required to decrypt this data.

The package flatmap-stream contains encoded data cleverly hidden in a test directory. This directory is not available in the GitHub repository but is available in the raw flatmap-stream-0.1.1.tgz package. The encoded data is stored as an array of parts. Each of these parts are minified/obfuscated and also encrypted to various degrees. Some of the encrypted data includes method names which could alert the malicious behavior to static analysis tools, such as the string _compile, which is a method on require for creating a new module. I’ve done my best to clean-up the files and make them human readable in the following two code samples.

Here is the first part. It isn’t as interesting and mostly appears to be a bootstrap function to load the second part. It appears to work by modifying a file name ReedSolomonDecoder.js from a submodule. If the file has already been modified, which is known to have happened if /*@@*/ appears in the file, then it does nothing. If it hasn’t been modified it not only modifies the file but also replaces the access and modified timestamps to be the original values. That way if you glance at the file on disk you would not immediately notice it had been modified.

And here is the more interesting second part. Some superfluous pieces have been removed to make the original intent more obvious.

This file monkey-patches functionality from the bitcore-wallet-client package, specifically the getKeys method of the Credentials class. It backs up the original function, then inserts code to transmit the credentials for a wallet to a third party server. That server is located at 111.90.151.134. These credentials can likely be used to gain access to users accounts and allow an attacker to steal money from the original owner.

The package makes several attempts to avoid detection. For example, it doesn’t run when using a test Bitcoin network, identified as testnet. Instead it only runs on the live Bitcoin network, named livenet. This could help avoid detection if an affected app runs acceptance tests against the test network. It also only appears to run the bootloader when a release build is being generated. It does so by looking at the first argument in process.argv and testing it against the regular expression /build\:.*\-release/, and returning if a match isn’t made. This argument is likely provided by some sort of build server.

How could this attack have been prevented?

It may be tempting to rely on tools which scan npm packages by way of static analysis. This particular attack encrypts the malicious source code to avoid detection. To protect against such an attack a different approach must be taken…

This particular attack appears to run in both a normal webpage as well as an application built with Cordova — a tool for converting web apps into mobile apps. The attack could have been prevented by making use of CSP (Content Security Policy). This is a standard for specifying which URLs a webpage can communicate with and is specified via web server headers. Cordova even has its own mechanism for specifying which third party services can be contacted. However, the Copay application appears to have disabled this feature.

CSP is a great tool for securing frontend applications. However, such a feature is not built into Node.js itself. Intrinsic is a Node.js package which provides the ability to whitelist which URLs an application can communicate with— much like CSP—however it can do much more powerful things as well. Intrinsic can be used to whitelist filesystem access, child process access, sensitive process attributes, TCP and UDP connections, and even fine-grained database access. These whitelists are specified on a per-route basis, making Intrinsic far more powerful than a firewall.

Interestingly, this attack on event-stream, wherein the attacker monkey-patches a sensitive function with a malicious one which makes outbound HTTP requests to an evil server, is exactly what we warned against in our previous post: The Dangers of Malicious Modules. These supply-chain attacks are only going to become more and more prevalent with time. Targeted attacks, like how this package specifically targets the Copay application, will also become more prevalent.