There is a security vulnerability in npm by default that enables writing a worm that can propagate to anyone doing an
npm install to a package that would contain an infected dependency (even if the dependency is deep).
This threat uses a combinaison of elements:
npm installinstalls a package and all its dependencies (and deeply) by default
npm install --savemakes that by default, dependency versions accept any later patch version of the same package
npm install, by default, runs all lifecycle scripts of all dependencies and these can be arbitrary bash commands
npm login, by default, saves the authentication credentials in
npm version patch ; npm publish.
Through the repeated annoying use of by default, the reader understands that if a worm is sent to npm, it will propagate because most people dont change defaults. Point 5 listed an example of what the worm can do to propagate, but of course once you have arbitrary access to the machine, you can just encrypt their disk and ransom for a bitcoin against giving back the data.
This is serious.
NPM response has been considered weak by some. It is. It does not change any default settings, so the threat is not really being addressed.
npm recommands an opt-in defense which is running
npm install --ignore-scripts which disables lifecycle scripts. It is suggested elsewhere to do
npm shrinkwrap to lock down dependencies or to log out systematically after having published. These are also an opt-in.
npm cannot guarantee that packages available on the registry are safe. If you see malicious code on the registry, report it to email@example.com and it will be taken down.
So, after the fact, when users machine have been infected by a worm npm takes the malicious package down? Too late, but thanks, lol!
As soon as a package is infected, people installing it will have their machines infected. npm cannot accept to be used to affect others machines. Even if npm maybe limits the propagation, malicious packages will be sent to npm.
Maybe the next person disatisfied with a name dispute resolution owning another heavily-dependend module will send malicious patch updates instead of just unpublishing them.
As packages will be more valuable, their author will be more likely to become targets of various attacks. Maybe someone will pay an author for the ownership of a module thats heavily dependend on to distribute malware. This has happened with Chrome extensions, no reason this wont happen with npm especially as long as there is no trust model for authors.
npm monitors publish frequency. A spreading worm would set off alarms within npm, and if a spreading worm were identified we could halt all publishing while infected packages were identified and taken down.
What about a patient worm? The publication frequency is exactly the same as the normal frequency and discrecy makes it hard to detect on users machines as well as hard to detect which packages are infected on npm. You can start playing the virus signature game but attack is always a step ahead of defense in this game and itd be a massive amount of resources spent only on this problem. Blaaah
Software should be secure by default, not an opt-in.
People who are coming to npm today and tomorrow have missed the blogposts and tweets. They wont opt-in, theyll be infected.
People who reinstall node/npm will forget to opt-in. Theyll be infected.
I am sorry, but the current insecure-by-default state of npm is irresponsible. Some default needs to be changed.
Lets review the list above:
npm install, lets keep this default.
People I dont necessarily trust a lot write scripts, post them as lifecycle scripts on npm and that runs on my machine. Why on Earth would these scripts have access to my entire filesystem, by default? This is an absurd amount of authority to give to random scripts downloaded from npm, who themselves tell us they cannot guarantee that packages available on the registry are safe.
This is a classic violation of POLA.
Quite often, we have enough context to know that things look really alarming from the outside are really not that big a deal or rather are no bigger deal than is already there and in an unfixable way in the CLI; the package script vulnerability is a good example of that. Thats just a cost of doing business with user-contributed content.
Red pill coming your way.
Like the joke above, Im only parroting the words of others here.
(these talks are long, but theyre worth your time, I promise. I have others if youre interested)
The folks in these videos have good metaphors for the state of software security. One of my favorite quote comes from Marc Stiegler:
Buffy Summers! In Season 3, her mother makes the criticism that every security person needs to pay attention to! Joyce says to Buffy: but whats your plan? you go out every day, you kill a bunch of bad guys and then the next day there is more bad guys. You cant win this way. You need a plan. And finally, Buffy in the last episode of the last season comes up with a plan: she changes the fundamental physics of her Universe to permanently favor the defender.
What could be lazier than forcing the other guy to play by your rules?
Why are we running commands that, by default, have the authority to publish npm packages on our behalf? Were playing the attacker game. Any npm command and lifecycle script should only have the authority to do its job and no more.
The first step is defining the appropriate amount of authority that lifecycle scripts should have. What are legitimate usages of lifecycle scripts? We can start with the following list:
(yes there is a single item, lets have a discussion on what that list should be)
So the lifecycle script needs read-write authority over the project directory. Cool! Lets give it only access to this specific directory and no other files!
wait! Why does it have write authority over
package.json? Never heard of a build script that needs to modify
package.json, lets only give read-only authority over this file and read-write over the rest.
I have a proof of concepts of this in the containednpm repo. It uses Docker because it was easy for me to write. Smarter people with more time on their hand will find more subtle solutions. The only point Im trying to make is that its possible, not that my quick implementation should be used or even a reference.
In the end, what happens is that if you run
npm install https://github.com/DavidBruant/harmless-worm/tarball/master --save, what happens is:
postscriptscript runs and modifies
package.jsonin a scary way
But when you run
./bin/containednpm install https://github.com/DavidBruant/harmless-worm/tarball/master --save, what happens is:
postscriptfails to edit
package.jsonbecause it only has access to a read-only version It would also fail to read your
$HOMEbecause it runs in a docker container and nobody gave access your
$HOMEto this container
I have to mention that there is zero copy happening. The
package.json that the contained lifecycle scripts see is the actual one. The creation of the
node_modules directory happens in the right directory directly, no temporary directory, etc. None of this is magics. Docker does the heavy lifting and Im just sitting on these shoulders.
What happens if the lifecycle encrypts the filesystem and wants to ransom for a bitcoin? It succeeds inside the docker container which contains few valuable data (only the project youre working on, hopefully, its versionned so you may not care too much losing it on this computer) container that is discarded at the end of the
In any case, all your other projects, your browser history, your emails and your
$HOME are safe without having you to pay back for them. My pleasure.
Thats the way we can change the rules of the game permanently in favor of the defender. Its possible to be a lot more secure when doing business with user-contributed content.
When I started this work and this blogpost, I was planning on only talking about the technical bits about how I smartly used a combination of docker, docker-in-docker, dynamic docker volume swap, dirty $PATH hacks based on the fragile assumption than lifecycle are started with
sh -c, etc.
But none of that matters. This is just a defense POC. Its possible, I did it, lets have the other more important discussion instead.
Im happy to make another blog post to explain the technical details (they should be straightforward to anyone familiar with docker and unix command line) or maybe get in a call or answer emails.
Hey great work on the POC! Im going to install your POC so my
nodeare safe by default!
My pleasure! This still needs more polishing but its good there is another secure opt-in against the worm and associated threats!
Regardless of the level of polishing, this defense will remain an opt-in and we need to change the defaults. This work needs to be merged into the official npm client to be any useful at scale. Security should not be an opt-in.
Security should not be an opt-in, so npm needs to be on-board otherwise, this is just yet another opt-in and does not really solve the problem.
Assuming there is interest in the exploration:
bin/containednpmuses docker and requires
sudoprivilege which is absurd. There are probably other tools for containement that dont require this and requiring everyone who wants node/npm to install Docker is a ridiculous requirement anyway)
npm cliunder a
npm install --safe. Each difference certainly means that the lifecycle scripts implicitely ask for authority that it hasnt been granted. Decide then whether:
npm install --unsafeand
npm install --safebecomes the default behavior of
Bim! Secure default! And no major disruption for anyone.
--unsafe provides a fallback if you want to get wormed or ransomed, but at least, youre opting-in for being attacked! Thats a much better news for everyone else.
I have to note that there is a cost related to having to maintain the list of default authority over time. I doubt it will be too much, but I know it cannot be zero.
Additionally, this default behavior for the CLI would act as negative incentive for anyone whod want to publish malicious packages. If they know it wont work for the majority of people by default, theyll certainly try to attack something else.
Things can be better; npm, lets talk!
Thanks Romain for an early review and convincing me to pursue in the direction of a blogpost that would be less technical and more about the threat and context. Thanks Thomas for the help trying to make containednpm worm on Mac