~ 7 min read
NPM Ignore Scripts Best Practices as Security Mitigation for Malicious Packages
A common supply chain security issue with npm packages is the capability of malicious or compromised npm packages to execute arbitrary code or OS commands during the package installation process. This has been a common vector and security concern for developers in the JavaScript ecosystem for many years now and has been exploited by attackers to compromise systems.
About npm postinstall Scripts
The npm package manager (the CLI, not the registry) has a feature called lifecycle hooks. These hooks are scripts that are defined in the package.json
manifest file and are executed at different stages of your projects installation process. One of the most commonly used lifecycle hooks is the postinstall
script (but there’s also preinstall
for example). This script is executed after a package is installed and can be used to perform additional setup or configuration tasks.
So to be practical, imagine the following scenario: your project depends on some third-party packages from npm, right? if you install a package example-dummy-package
and this package itself defines in its own package.json
file a postinstall
run-script hook, then that command will run when you run the following in your project:
This behavior applies to both direct dependencies or any transitive dependencies in your dependency graph that may define a postinstall
(or other lifecycle run-script hook).
The Security Risk of npm postinstall scripts
As you can already imagine by now, the significant risk of the npm package manager allowing third-party dependencies to execute random commands means that any maintainer and any package in your dependency tree may run any operating system commands during your own npm install
process.
While the risk is somewhat contained because the command would still only run with the privileges of your own user (or the CI user) that runs the npm install
command, it is still significant.
Examples of postinstall scripts security incidents
There are countless examples of npm packages that were published to the npm registry with malicious intents and having postinstall
scripts that would run malicious commands on the user’s machine. Here are a few examples:
- eslint-scope - a package that was published with a malicious
postinstall
script that would steal the user’s npm credentials. This was indeed part of the official ESLint project that you probably heard of already. One of the package authors was compromised and the attacker published a new version of the package with the malicious script. - crossenv - a package that was published with a malicious
postinstall
script that would run a command to steal the user’s environment variables. Note, this package was a typo-squatting attack that was meant to look like the popularcross-env
package from the popular JavaScript developer Kent C. Dodds.
Specifically, the case of crossenv
shows the significant and security risk of postinstall
scripts. Meaning, even if the system user used to install npm packages isn’t the root user, it still has access to sensitive information such as environment variables, .env
files, and other secrets that could be used to breach the system and would be easily exfiltrated by a malicious postinstall
script.
What is the npm ignore-scripts
flag?
Ok, so how do we fix this postinstall
scripts problem?
The npm CLI has a flag called ignore-scripts
that can be used to prevent the execution of any lifecycle hooks defined in the package.json
file of the packages you are installing. This flag can be used with any npm command that installs packages, such as npm install
, npm ci
, npm update
, etc.
Here’s how you can use the ignore-scripts
flag:
This will install the package you specify without running any lifecycle hooks defined in the package’s package.json
file of that package or any of its own transitive dependencies.
đź‘‹ Just a quick break
I'm Liran Tal and I'm the author of the newest series of expert Node.js Secure Coding books. Check it out and level up your JavaScript
Yarn support for the —ignore-scripts flag
Yarn uses the enableScripts configuration flag to control whether lifecycle scripts are executed during package installation. By default, this flag is set to true
which means that lifecycle scripts are executed. You can set this flag to false
to disable the execution of lifecycle scripts during package installation.
The enableScripts
configuration is set in the .yarnrc.yml
file.
Apply the ignore-scripts
flag by default
Better than passing the --ignore-scripts
flag every time you install a package, you can configure npm to always ignore scripts by default. You can do this by setting the ignore-scripts
configuration option in your .npmrc
file:
This is useful because it saves you from having to remember to pass the --ignore-scripts
flag every time you install an npm package. It also helps to enforce the best practice of always ignoring scripts when installing packages from npm.
Bonus, if you push this .npmrc
file to your project’s repository, you can enforce this best practice across your team and CI/CD pipelines so no one else needs to be bothered with remembering to pass the --ignore-scripts
flag either.
To take it further, I also recommend applying the ignore-scripts=true
configuration option for npm on the system level. This way, you can ensure that it is the default across all projects you use or clone on your system.
This is how you configure npm to ignore scripts by default on the system level:
Side-effects of using ignore-scripts
So you should be aware that some third-party packages on npm may require their postinstall
scripts to run in order to function correctly.
Some people will argue:
But what if you depend on legitimate packages that use postinstall scripts? 🤷
For example, a package may use a postinstall
script to compile native addons, or to set up configuration files, or to perform other necessary setup tasks. A few examples are these packages:
- bcrypt
- node-sass
- sharp
Meaning, if you install any of these packages and you default to not allowing post-install scripts then these packages may not install correctly or may not work as expected.
So should you ignore scripts by default or not?
It’s a trade-off between security and functionality. My opinionated recommendation is to always use ignore-scripts
by default and only disable it on a case-by-case basis when you trust the package maintainer and you know what the postinstall
script does.
If the third-party package requires postinstall
scripts to run and is a regular part of the project, you can use LavaMoat’s allow-script package to manage an allow-list of trusted packages.
Another reason to consider disabling postinstall
by default and prioritizing security is due to a prior academic research study that showed that about 2% of the npm registry actually has postinstall
scripts defined. This means that the majority of packages you install won’t be affected by this change.
“we found 2.2% (33,249) of packages use install scripts, indicat- ing that 97.8% of packages may follow npm recommendation of not using the install script as best security practices.”
How to test for postinstall
scripts
If you want to audit your project’s configuration and security posture for postinstall
scripts, you can use a dummy test package made available by the LavaMoat project maintainers called @lavamoat/preinstall-always-fail
If this package fails to install then it means that your project is configured to allow postinstall
scripts (not a good sign of security). If it installs successfully then it means that your project is configured to ignore postinstall
scripts (a good sign of security).
Best Practices for npm ignore-scripts
Here are some best practices for using the ignore-scripts
flag with npm:
- Use
ignore-scripts
by default: Always use theignore-scripts
flag when installing packages from npm. This will help mitigate the risk of malicious packages executing arbitrary commands on your system. - Use
ignore-scripts
in CI/CD pipelines: You need to install dependencies in your CI/CD pipelines so make sure to always use theignore-scripts
flag to prevent any malicious packages from executing arbitrary commands on your CI/CD system (and potentially stealing environment variables, spinning up cloud infra and using it for crypto mining, etc). - Use npq: If you want to enforce the use of
ignore-scripts
in your project, you can use thenpq
tool which is a wrapper around the npm CLI that will automatically run heuristics to collect security information about the packages you are installing and will automatically warn you if a package has apostinstall
script.