rfcs icon indicating copy to clipboard operation
rfcs copied to clipboard

[RRFC] Approved package lists

Open johndiiorio opened this issue 2 years ago • 4 comments

Motivation ("The Why")

Today (10/22/21), ua-parser-js was maliciously compromised, a package with millions of downloads per week. The npm team responded quickly, but there was a 4-hour window where many people could have been affected. This includes those with with a package-lock.json if they were modifying dependencies. This RRFC aims to put an end to compromised transitive dependencies.

"The How"

I propose that npm implements a new feature called "Approved package lists" (name of course up for discussion). This would be an entirely opt-in feature for teams where security is critical. Here's how it works:

  • The npm package manager is now aware of a new special file, package-approvals.json. This would live along side package.json and package-lock.json. It would be excluded from files published to the registry on npm publish.
  • This file would have the following structure (for the initial version):
{
    version: number,
    packages: {
        [<package-name>]: string[],
    },
    extends?: string; // file path or URL
}

In each array associated with the package, there would be a string of version numbers, e.g. ['1.0.0', '1.1.9', '2.3.4']. Importantly, there would be no support for ranges of versions.

  • A number of commands would be added to the npm cli, such as npm approve <package-name>@<version>, npm unapprove <package-name>@<version>, and npm approve --all-current-packages. The npm approve --all-current-packages would have a Yes/No prompt to confirm this choice.
  • The package.json file would support a new key: approvals?: string. This optional key could either be a relative/absolute path to a file with the package-approvals.json structure or a URL to retrieve the file.

The idea behind the above proposal would be to change how npm install/npm ci works, and would apply to workspaces. On install, if npm would resolve a package (including transitive packages) but the package version does not exist in the approvals list, npm would error. Importantly, this would occur before any files are written to disk and package scripts (e.g. postinstall) are run. The error could include a list of all packages that don't satisfy the approvals. Developers could resolve this error by running npm approve <package-name>@<version> for a single one, or (if they are really sure their current dependencies are safe), run npm approve --all-current-packages. These convenience scripts would modify the local package-approvals.json file.

Developers would be able to leverage this new file by guaranteeing that they never use an un-audited dependency. Each team of developers could perform their own audit of the dependencies they wish to use, then add each one to the approvals list. This could be a time consuming task, but worth it. To alleviate some of the burden, this file could be hosted somewhere accessible by the internet, so teams or individuals could share their configuration via the approvals package.json field. Additionally, the package-approvals.json has an extends field that could resolve and recursively extend other package-approvals.json files. Obviously there are too many npm packages to have a single community-hosted package-approvals.json file for everyone, but the community could create shared ones for different use-cases.

Example

{
    version: 1,
    extends: 'https://example.com/path/to/some/package-approvals.json',
    packages: {
        lodash: ['4.17.19', '4.17.21'],
        react: ['16.0.8', '17.0.2'],
        // and many more
    },
}

I feel that something like this would greatly improve security in the npm community. Please let me know what you think, or if there is a similar RRFC like this already.

johndiiorio avatar Oct 23 '21 02:10 johndiiorio

Here is another alternative, basically just white-listing packages that can run scripts: https://github.com/npm/rfcs/discussions/80 [link to discussion / explanation]

(not saying it's good or bad, just wanted to mention this other idea)

sandstrom avatar Oct 25 '21 09:10 sandstrom

My idea is like a "disapproved package lists" [RRFC] Allow set forbidden dependencies in package.json 😂

harrisoff avatar May 13 '22 03:05 harrisoff

@harrisoff I like your RRFC, though I think both should be included with npm. Yours is sort of like a package manager level version of eslint-plugin-import to guard against certain packages sneaking in, be it malicious or just unwanted dependencies.

The hope with my RRFC (or a similar proposal) would be to change the trust mechanism from "trust anything" to something stricter. In my dream world, an open source project (or multiple projects) would take the initiative on vetting packages and updating their own master list of approved packages. This is somewhat analogous to the DefinitelyTyped repo. Of course, the downside is that if package A releases a patch to fix a bug, it'll take a little bit for upstream to vet it.

Going further and guesstimating wildly, there's probably only ~5000 critical packages to the npm ecosystem. If you have a complicated project, there's a good chance you'll end up depending on some combo of @babel/core, lodash, rollup, webpack, typescript, maybe react or vue, acorn, jest, etc. Each of these may have large dependency trees, but crucially there's overlap in the transitive dependencies. A project may require a few less popular packages, but probably only a handful and likely to be explicitly declared by the user. IMO I'd love to know if I'm accidentally depending on some obscure package with 5 downloads/week maintained by one guy in Antarctica - I'd try and see if I could drop it.

Next, I'm going to make the (unsubstantiated) claim that (usually) the more downloads/week a package has, the less often it updates. Let's use chalk as an example. It has 136 million downloads/week; it's a high profile target and having it compromised would be bad. However, it's only been updated 33 times in 9 years. Going back to my dream world, if chalk wanted to release a new version, they'd do the following:

  1. chalk would make all necessary changes for the new version, but not publish it.
  2. chalk would give the security project a heads up, along with their changes (and corresponding hash)
  3. Security project would vet the diff between versions and basically say "changes with this hash looks good" and update the master list
  4. chalk publishes the new version. Both users who have configured their setup to trust the security project and users who don't care about this could immediately download the package.

Packages that aren't as popular, say <10,000 downloads/week, could opt to just publish the new version right away without waiting. Users could either wait for the project to review it or check it out themselves. Again, this whole concept would be completely opt-in. It's a shift from the existing behavior, but I think it's worth a discussion.

johndiiorio avatar May 13 '22 05:05 johndiiorio

Two additional features to this RFC would be interesting to investigate :

  • be able to set a rule for transitive/ non transitive dependency
  • be able to set a rule for dependency type (dev, peer, normal)

jpolo avatar Jul 19 '22 15:07 jpolo