Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make npm install scripts opt-in #488

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
49 changes: 49 additions & 0 deletions accepted/0000-make-scripts-install-opt-in.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Transition to a model where install scripts must be explicitly allowed during the install process.

## Summary

Instead of by default always running the install scripts (`preinstall`, `install`, `postinstall`, `prepublish`, `preprepare`, `prepare`, `postprepare`) if they are present during the install process, provide flags to require users to explicitly allow them to run, either whoelsale as "one big swtich", or on a package by package (and optionally version by version) basis. Also provide matching `npm config` options to do the same globally and permanently instead of on every install.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please clarify if scripts are ignored or fail the install if they are not allowed to run.


## Motivation

Install scripts that can run just about anything by default pose some pretty serious security considerations, and these are inreasingly moving out of the theoretical realm and becoming actively exploited. See for example here: https://therecord.media/malware-found-in-coa-and-rc-two-npm-packages-with-23m-weekly-downloads/.

At the same time, we have developed better techniques for many of the most common use cases for install scripts in the many years since npm originally included then. In particular, [N-API](https://nodejs.org/api/n-api.html) offers a compelling alternative to binary packages that are built on the users' computer. However, even before this, many packages are choosing to just pre-build for multiple platforms ahead of time to handle most of the common installation targets and make the install process easier on the user in general.

## Detailed Explanation

I propose the following flags and changes in behavior:

### npm install
1. By default, no scripts are run during the `npm install` lifecycle: https://docs.npmjs.com/cli/v7/using-npm/scripts#npm-install
2. The `--allow-scripts` flag is added to `npm install` which allows users to enable scripts for a specific package at a specific version, which is the only truly safe thing combination (especially given a `package-lock.json` file that is ensuring the integrity of the tarball in question):
- `npm install --allow-scripts canvas@1.0.0`: Install the contents of `package.json`, only allowing `canvas` version 1.0.0 to run install scripts. Perhaps it should warn if no install scripts are present?
- `npm install canvas@1.0.0 --allow-scripts canvas@1.0.0`: Installs `canvas@1.0.0` to the current `package.json`, and allows it to run install scripts. Perhaps it should warn if no install scripts are present?
- `npm install canvas@1.0.0 --allow-scripts canvas@1.0.0 --allow-script ramda@2.0.0`: Installs `canvas@1.0.0` to the current `package.json`, and allows it to run install scripts, as well as allowing ramda@2.0.0 to run install scripts. Perhaps it should warn if no install scripts are present?
3. The `--disallow-scripts` option should also be added that does the opposite of `--allow-scripts`. This is necessary since later I will discuss global settings for allowing packages to use install scripts, and so this lets you ignore that setting if you so choose.
4. The `--allow-scripts-unsafe` option is added for more convenient, but more dangerous, handling of install scripts:
- `npm install --allow-scripts-unsafe canvas`: Install the contents of `package.json`, allowing any version of `canvas` to run install scripts.
- `npm install --allow-scripts-unsafe "canvas@1.0.0-2.0.0"`: Install the contents of `package.json`, allowing any version of `canvas` in the semver range 1.0.0 to 2.0.0 to run install scripts. While strictly safer than the unranged version above, it still runs the risk of new vulnerable packages being snuck in in that range. For example, if an account is compromised, and a patch version is published to every major version that is present.
- `npm install canvas --allow-scripts-unsafe canvas`: Installs the latest version of canvas to the current `package.json`, and allows it to run install scripts.
- `npm install canvas --allow-scripts-unsafe canvas --allow-unsafe-scripts ramda`: Installs the latest version of canvas to the current `package.json`, and allows it to run install scripts for canvas and any version of ramda it encounters.

### npm config

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The set of packages that can run scripts should be tracked in source control so developers or CI agents can install the projects dependencies without making decisions about which scripts to run. Choosing scripts to run should be done when adding or updating dependencies, not when installing packages that are already dependencies.

I think package.json is the best location for this option.

  • package-lock.json is ephemeral and may be deleted to get a fresh install.
  • .npmrc may or may not be tracked in source. 1

Footnotes

  1. If it is checked into source developers need to avoid checking in personal changes. For example a dev needs a proxy for one project but not another. If they set the config in project npmrc they must not commit that change, if they set the config in their user npmrc they must remember to remove it before working on another project. I should write a RFC for multiple project npmrc's.

We should add the ability to configure npm globally to allow packages to make use of scripts. This can be useful for packages you install a lot, or for packages you are working on:

1. `npm config set allow-scripts-canvas@1.0.0 true`: This will allow canvas version 1.0.0 to install scripts whenever `npm install` is used.
2. `npm config set allow-scripts-unsafe-canvas true`: This will allow any version of canvas to install scripts whenever `npm install` is used. It should also probably warn on each case.

## Rationale and Alternatives

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add a scope section, your comment here is a good starting place
#488 (comment)


The vast majority of packages do not use install scripts, and they provide an easy vector to immediately affect thousnads of users if you are able to take control of a package author's account. While not a perfect solution, this would dramatically reduce that capability, and is probably the correct default instead of allowing any 20-levels-deep subdependency to run any artbitrary script on your system.

## Implementation

I imagine this should hopefully not be harder than adding some if statements around the right lifecycle sections of install.

## Unresolved Questions and Bikeshedding

Two open questions for me:

1. Having npm ship with a grandfathered list of "known safe" package versions. For example, every currently shipping version of canvas. That way, for important large packages, this is a "from now on" requirement. Alternatively, there could just be a transition period where npm just warns about script, and then afterward we make it actually refuse to run them without the proper flags.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

N.B. grandfather clause has a pretty nasty history, I try to use 'legacy'.

grandfather clause
a provision in several southern state constitutions designed to enfranchise poor white people and disenfranchise Black people by waiving high voting requirements for descendants of men voting before 1867

2. I think the automatic node-gyp stuff should be under this umbrella as well. That is to say, node-gyp would only run when gypfiles are detected if the `allow-scripts` flag allows it, since you can also techncially put whatever you want in there I believe, and not doing this would simply mean that any malicious behavior would be transitioned there.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
2. I think the automatic node-gyp stuff should be under this umbrella as well. That is to say, node-gyp would only run when gypfiles are detected if the `allow-scripts` flag allows it, since you can also techncially put whatever you want in there I believe, and not doing this would simply mean that any malicious behavior would be transitioned there.
2. I think the automatic node-gyp stuff should be under this umbrella as well. That is to say, node-gyp would only run when gypfiles are detected if the `allow-scripts` flag allows it, since you can also techncially put whatever you want in there I believe, and not doing this would simply mean that any malicious behavior would be transitioned there.
## Prior art
- run scripts from allowlist [allow-scripts](https://www.npmjs.com/package/allow-scripts)
- run scripts from allowlist [allow-scripts from lavamoat](https://www.npmjs.com/package/@lavamoat/allow-scripts)
- tools to prevent/detect scripts running when not wanted [preinstall-always-fail](https://www.npmjs.com/package/@lavamoat/preinstall-always-fail) [pentest-my-ci](https://www.npmjs.com/package/@naugtur/pentest-my-ci)
- reviewing which scripts need to run to help build allowlists [can-i-ignore-scripts](https://www.npmjs.com/package/can-i-ignore-scripts)