The NPM project has formally acknowledged a long-standing security vulnerability in which it is possible for malicious packages to run arbitrary code on developer's systems, leading to the first NPM created worm. In vulnerability note VU319816, titled "npm fails to restrict the actions of malicious npm packages", Sam Saccone describes the steps needed to create a worm and to allow it to spread automatically. Although this was reported in January 2016, the issue has been widely known about and has existed since the initial release of the NPM repository manager.
The root of the problem is that NPM modules have associated scripts that can be executed by NPM when they are installed. These include plenty of opportunities to execute code when the module is downloaded from NPM:
preinstall
- run before the package is installedpostinstall
- run after the package is installedpreinstall
- run before the package is uninstalleduninstall
- run while the package is uninstalledpostuninstall
- run after the package is uninstalled
These scripts exist to make publication of NPM modules easier and to perform post-processing of the contents before it is used. For example, some modules might be originally written in CoffeeScript or TypeScript that require a translation step, or may be written in ES6 and require a Babel translation to run on current browsers. In addition JavaScript libraries are often minfied (to make them smaller) which is usually an automated step as well. Since JavaScript is interpreted, tools like Make are not used and so the npm scripts are used to do the heavy lifting.
NPM misuses these scripts to perform not just build time validation but client-side execution when modules are downloaded and used by a client. For example, the left-pad fiasco (covered by InfoQ yesterday) was resolved by republishing the modules; however, if such widely-used modules (such as the true module, whose purpose is to only print true
) has an infected scripts block then code execution across millions of machines is a real possibility.
These scripts allow the creation of NPM worms, which go on to infect other packages and spread accordingly. NPM package developers can publish their packages (using npm
, of course) to the NPM repository using standard credentials. In order to facilitate publication, it's possible to npm login
into the repository and stay signed in for an arbitrary amount of time, after which any packages may be published to the repository. As such, once an npm developer's machine is compromised, the worm can scan for any additional packages and then republish them (using the developer's existing credentials) with the worm injected into the scripts block of the newly infected packages.
Furthermore, the package manager itself uses JavaScript execution meaning that just resolving a package's dependencies can result in arbitrary code execution. A proof of concept was published which used a variable for the package name, thus appearing to masquarade as any package. In true open source spirit, the proof of concept is MIT licensed:
A="$1" echo '{ "name": "'"$A"'", "version": "2.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" }' > package.json npm publish
If a package developer downloads this package and executes the script then it will execute index.js
, which in turn can execute arbitrary JavaScript on the end machine. The proof of concept shows how to run sudo rm -rf /
which will cause problems for those without backups.
The problems are further complicated in that the NPM repository allows anyone to repurpose an existing package, simply by publishing a new version into their space with the name identifier. Unless developers explicitly lock down the version that they depend upon, automatic upgrades to the latest version are trivially possible and are likely to see further attacks in future.
The NPM response so far has been rather weak, disclaiming responsibility for scanning for malware while pointing out that developers using modules are themselves to blame for the infection. Yet the whole infrastructure is set up to allow anyone to own packages and replace them with arbitrary JavaScript; even if the scripts themselves are excluded (by using npm install --ignore-scripts
or npm config set ignore-scripts true
) then it is still possible to replace the body of a JavaScript file with require('shelljs').exec('rm -rf /')
and have the same devestating effect when run. Furthermore leaving an npm session logged in means that any application, whether NodeJS based or otherwise, can easily publish scripts on behalf of that user without their knowledge.
What recent events have shown is that the meteoric growth of JavaScript and the use of NPM on the server was given the same focus on security as the JavaScript language itself. The only surprisng thing about the whole affair is that it has taken this long for the house of cards to have fallen down.