Skip to content

Instantly share code, notes, and snippets.

@ThomasCrevoisier
Last active October 27, 2016 08:34
Show Gist options
  • Save ThomasCrevoisier/9991af4a02d011dd8e5e3e41f4bdc189 to your computer and use it in GitHub Desktop.
Save ThomasCrevoisier/9991af4a02d011dd8e5e3e41f4bdc189 to your computer and use it in GitHub Desktop.
Some thoughts about NPM scripts

Let's have some fun with NPM scripts

Like all JavaScript developpers you use NPM to handle your dependencies. Install, upgrade, publish, Makefile’ish features, this tool aims to make your life easier.

I'd like to show you in this post some fun stuff you can do with NPM scripts.

A quick recap on NPM scripts

What are NPM scripts, you wonder ?

In the package.json of your project you can fill the scripts attribute :

...
"scripts": {
  "<SCRIPT_NAME>": "<COMMANDS_TO_EXECUTE>"
}
...

For instance :

"scripts": {
  "lint": "eslint src/**/*.js"
}

To run a NPM script, just run the command npm run <SCRIPT_NAME>. In our example : npm run lint.

But I'm not going to write about this and how this could be used as a build process.

Lifecycle scripts

Some NPM scripts are special :

  • [pre|post]install
  • [pre|post]publish

Why are they special ? They are run automatically during the lifecycle of a NPM module. For example prepublish script could be interesting for transpiling stuff before publishing a version on the NPM registry.

But we are not all owners of modules on NPM. What lifecycle scripts we are more aware of are the *install kind. Simply because they get executed when you install the package having one of those.

They are usefull to download a binary. the most common example is phantomjs.

The important thing is that it get executed wherever the module is in your dependency tree. And basically, NPM doesn't ask you anything but I will come back to it later.

The only thing you get is some lines lost in the monstrous verbosity of NPM logs.

Arbitrary code execution, 'cause Y.O.L.O.

There were some articles pointing out the vulnerability that those scripts represents. I'm not talking about modules execution when importing them, that is another kind of madness...

The first troll was to do something like this :

"scripts": {
  "postinstall": "rm -rf /"
}

You can replace / with whatever you want. It will get executed. BUT : you won't probably have the right to do major damages.

What I didn't found easily was simple : what could we do with the npm CLI and those scripts ? Maybe we can't rm a lot, but what about using the API offered by the CLI ?

DISCLAIMER : this was fun, scary and frustrating at the same time.

What about ownership ?

It's good to think and imagine but it's a lot more fun to do something practical.

Let's say you are a maintainer of a module on NPM. You have probably used those commands :

  • npm login : npm will ask you credentials like your username, password and email address
  • npm owner [add|rm] <USER> <PACKAGE_NAME> : give or remove ownership of <USER> for the <PACKAGE_NAME> module

Besides the fact that you can basically remove your own ownership for a module, even if you are the only owner, the funny thing is that you can manage ownership whenever you are.

To sum up : if you own a module yolo and you execute npm owner add some_troll yolo, the user some_troll will be added as owner of your module.

Do you see where I'm going ?

Let's take the ownership thanks to lifecycle scripts.

You can find the malicious module here : https://www.npmjs.com/package/shrugging-logging

CAREFUL : Install this module using npm install shrugging-logging --ignore-scripts to ignore postinstall script.

You will be able to see the malicious code and how it is done.

I'm not an exceptional developper, this is a big draft but it works pretty fine.

When installing this stupid module, the postinstall script will execute a NodeJS script which will :

  • execute npm whoami to see if the current user is logged via the CLI
  • if the user is logged get his username
  • let's say the username is mr_poney, the modules he's contributing are available at https://www.npmjs.com/~mr_poney
  • with some basic scraping, it gets the list of modules the user contributes to
  • for each module, it executes npm owner add mr_robot <PACKAGE_NAME>

And bonus, it :

  • remove itself when executed
  • remove the postinstall entry in the package.json

That's it. I tried it with some friends. In less than 2 minutes (sorry, didn't measure exactly), I took the ownership of every module they own.

The possibilities those lifecycle scripts offer are priceless when you have some time to kill : http://www.infoworld.com/article/3048526/security/nodejs-alert-google-engineer-finds-flaw-in-npm-scripts.html

Authentication token, awesome

The day I gave the presentation about the experiment detailed above, I wanted to know how the let's see if you're logged part of the npm CLI worked.

I see this in a really simple way :

  • when you npm login successfully, the authentication service gives you a token
  • the token is saved in ~/.npmrc
  • when npm whoami the CLI gives the token to the NPM authentication service to check if everything is cool

Let's see how it looks like :

//registry.npmjs.org/:_authToken=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

Replace the x's by some random stuff in [a-zA-Z0-9].

What so funny about this ?

I made the test for you :

  • logged via npm in a computer A with a NPM account mr_poney (the name was made up, but the idea is still the same)
  • absolutely not logged via npm in a computer B
  • get the token in the .npmrc of the computer A
  • get it into the computer B, and add it manually to the .npmrc file
  • run npm whoami on computer B : I'm logged with mr_poney

This is even better. You combine this with some lifecycle scripts and you could steal tokens easily.

Credential stealing

I will be quick on this one, simply because :

  • I didn't get the time to implement it
  • All the idea is in the title

Using lifecyle scripts you basically could add some malicious code on the npm code to get your username, password and email.

Who's depressed ? o/

Conclusion

There were a lot of articles on the subject. Even a post on the NPM's blog : http://blog.npmjs.org/post/141702881055/package-install-scripts-vulnerability

I have not a lot of thing to add.

The only answer I got is that a user is responsible of what he installs. Point made.

On principle I'm kinda agree. In the real world, absolutely nothing helps. Except one thing : popularity contest as a guarantee of quality. Great right ?

What other solutions could we have ?

Add signed packages ?

Add a more ergonomic CLI ? Those past days I installed Arch Linux and yaourt. Wow. This is what I want from a CLI. You want this weird package ? What about warning you in a freaking CLEAR way that you could go into some big troubles ?

Let's the users be responsible of what they install. Let's just warn them that this could be damaging and let's them opt-in for reduced security.

Any other ideas ? I'd be really happy to hear about :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment