Understanding and protecting against malicious npm package lifecycle scripts
This article explains the importance of understanding how npm lifecycle scripts such as
postinstall work, how they could, (and have been) used maliciously, and offers some advice on what you can do about it as part of your npm project health.
postinstall lifecycle script, meaning each user who installed the package potentially had their npm registry login details sent to a remote address by the malicious script. The official npm incident report can be found here, but the important detail is that unknowingly, users running the familiar
npm install command had no idea that this malicious code was being executed during the
postinstall step. As these scripts run by default, any background tasks they execute are often hidden from the user, who is only looking for feedback on whether or not the new thing they installed is ready.
// package.json file"name: "express-example-package",
"postinstall": "node malicious.js"
If the above package.json file was an example package on the npm registry, any user who decided to
npm install express-example-packagewould have the above
postinstall script executed - the contents of this script file could be anything! The same behaviour is true for the scripts
postuninstall. The npm reference for these scripts are available here.
In reality, preventing this issue is much harder than one simple solution. These lifecycle hooks are an important feature - they can help set up packages in complex ways and perform important cleanup or preparation tasks, so it can be limiting to simply opt out of running these hooks. Due to this, the recommended approach to prevent this issue will always be to 1) review dependencies carefully, and use a lockfile to prevent auto-installing new packages
2) Opt out of running scripts on install
When installing a package, you can chose to opt out of running scripts using the
ignore-scripts option in npm or Yarn:
npm install --ignore-scripts
yarn add --ignore-scripts
Or, if you never want to run these scripts when installing packages, you can modify your global npm configuration:
npm config set ignore-scripts true
yarn config set ignore-scripts true
Note: this setting could also be added to a project’s
3) Audit current modules script use
I found a lack of tooling available to see which dependencies in the current dependency tree are currently running lifecycle scripts, so have published a cli tool to view this information. It is available on npm.
To use the tool, you can install it globally
npm install npm-viewscripts -g .
Inside a project directory, running
npm-viewscripts will provide a list of any dependencies currently installed which have, and will continue to run a script on install or uninstall.
This type of vulnerability is not a fault of npm, as these lifecycle scripts are a very helpful feature for package management, however the risks need to be understood by users. It could be argued that the issue has a wider scope with npm due to the large size of a typical Node.js application’s dependency tree, and the philosophy of building and sharing and consuming many small and discrete modules.
I think it is important that the community, and in particular maintainers of popular packages take precautions to prevent these attacks from continuing - the best prevention against this is to enable 2FA for all npm accounts with write access to packages. With this enabled, any npm credentials which are stolen by one means or another have less harm on consumers of the package, but that of course does nothing to prevent a trusted maintainer suddenly going rogue.
npm are doing some great work to reduce the risk of these types of attacks by reducing typosquatting, and offering free security tooling such as
npm audit .