rfcs
rfcs copied to clipboard
Make npm install scripts opt-in
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 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.
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 switch", 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.
Hello new comers from the mailing list π
This is a gently reminder that discussions and decisions are not taken after thumbs up and down or reactions, but after respectful conversations to align, eventually agree, and move forward.
I'll save you some time, this is exactly what happened here too, so you might want to skip right 'till the end of this discussion to screen latest chats and see were we've headed, thank you π
Install scripts that can run just about anything by default pose some pretty serious security considerations
This is nothing new in the software world, to be honest, when you install pretty much any software in Windows it asks you if it's OK to give this software the rights to configure the OS on your behalf and everyone is like "sure thing, whatever mate, let me move on" ... and here there's no difference ... except we miss the TRUST around packages.
What is trust? The fact a package has every allowed publisher behind 2FA to start with, so that publishers are accountable.
Every example to date shows that everyone can steal and publish on behalf of everyone else any kind of crapware, and yet the proposal here is to "provide yet another flag"?
What is a flag good for, when it can become a configurable default? It would solve nothing around the well known issue.
If there's anything npm should do, in my opinion, is to promote 2FA modules any way you can:
-
npm info module
shows a special star, flag, check, about security/accountability because of 2FA - the site itself down-votes any search if the module is not 100% behind 2FA (or its developer/s)
- a warning is shown on install ... or ... no pre/post install script is allowed if the module has not 100% 2FA publishers
Now this is thinking in a way that will stop causing the same accident over and over, where developers reputation also could play a role, so that trust become the way to move forward, as opposite of limiting all npm functiomalities because "evil happens".
Thanks for listening, and I hope yet another flag won't be the only move here π
P.S. we've been there before with HTTP (not S) and credit card payments or any sort of man in the middle attack, I don't understand why it's so difficult to see how much value a 2FA only ecosystem could bring to the npm's plate
This is nothing new in the software world, to be honest, when you install pretty much any software in Windows it asks you if it's OK to give this software the rights to configure the OS on your behalf and everyone is like "sure thing, whatever mate, let me move on" ... and here there's no difference ... except we miss the TRUST around packages.
This is actually a very different situation, as you are comparing the behavior of non-technical users installing non-technical software, vs. developers who do generally care (if allowed to) about the software they use. The other critical difference this analogy misses is that a very large portion of package installs are done not by humans, but in an automated fashion. So it is not a bunch of people saying "OK whatever," it is a fleet of CI machines repeatedly pulling packages and by default running scripts that could compromise them. A package going from not running an install script to running an install script between CI runs should IMO absolutely fail the test: there is a very bizarre change in side effects taking place, and test failures are an expected part of test suites (it's the whole reason they exist), and at that point you can issue another commit that adds the explicit approval of the package, or, as with what happened earlier today, say "wait a minute, none of these should be installing", and avoid having a bunch of junk infect your CI machines.
Another important piece of context is that vast majority of people will actually see no change, as a tremendously small amount of packages on npm today use this feature. If this was some critical package.json field that 10% of packages used or something, then that would be one thing, but the reality is that when we last checked every package (literally every package on npm) at RunKit, the percentage was vanishingly small.
Additionally, many of the packages that do use this legitimately now have better alternatives. A clear example is transpilation that happens on install instead of on publish (which additionally means we're wasting a bunch of energy retranspiling the same code over and over instead of shipping the "built" package). As mentioned in the PR, N-API also provides a better solution to actual binary packages.
What is a flag good for, when it can become a configurable default? It would solve nothing around the well known issue.
If you read the actual configuration options provided, you will see that there isn't a blanket option, only allowing you to globally allow "safely" a specific version of a package, and "unsafely" any version (or range) of a package. In other words, if you encounter that one random package that you know and love that has an install script, you can enable that one "from now on", which makes sense, you are saying "I've checked this package version, I'm good with it". This is very different than saying "I don't care about scripts running just do whatever with anything".
Thinking about the distinction between "automated" installs vs. "user drive installs" also informs the thinking around configuration vs. flags, and why both are provided to some extent: it makes total sense that one would have a different risk model for an individual developer's computer vs. a CI or production machine. If a developer accidentally installs something malicious on their computer, that could be relatively contained. However, that can be a huge profit loss if it manages to install on a production machine. So it makes a lot of sense to use the most explicit flags in the repo, but for individual users to be able to globally turn on certain packages for convenience.
Incidentally, you can even imagine a model where when running npm install
interactively it can ask you about packages instead of failing (the same way apt asks you about disk usage), that way this process has even less friction.
Every example to date shows that everyone can steal and publish on behalf of everyone else any kind of crapware, and yet the proposal here is to "provide yet another flag"?
The proposal is to make unexpected behavior default-off. It's like getting angry that browsers now require a iframe attributes to explicitly enable certain features -- no, it should have always been that way, and the people working on it (developers, not end users) are perfectly equipped to understand and update their code. Except again, most people will not even need to use this flag, as the legitimate use cases are small, and only growing smaller. Again, to the point where if this were implemented with a grace period to update packages, very quickly there would be very few packages that "need" this flag. I would argue that this would for example take much less time than the ESM transition that is currently underway. Also, as mentioned in the PR, if the flag is really that bothersome, you can globally allow packages to install.
Now this is thinking in a way that will stop causing the same accident over and over, where developers reputation also could play a role, so that trust become the way to move forward, as opposite of limiting all npm functiomalities because "evil happens".
Trust doesn't fundamentally solve the problem of purposefully malicious actors. If someone steals your 2FA keys and publishes a malicious package, no amount of "ratings" or "stars" in npm info can account for that, especially not before hand. There are two issues at play here: accountability (how to deal with things once they go wrong), and security (how to prevent things from going wrong in the first place). There doesn't exist an accountability system on Earth that would make me feel comfortable with giving browser JavaScript access to my filesystem by default. I want it sandboxed, no matter what other measures are taken.
you are comparing the behavior of non-technical users installing non-technical software, vs. developers who do generally care
beside the fact we don't have data around developers that really care VS developers that copy and paste whatever they find on SO or elsewhere, this proposal would break genuine software like this which needs mandatory postinstall
, and everything not having it allowed by default would just break.
A breaking change like this in npm needs to take into account all use cases to date (Electron based Apps too), not just CI, but I'll read the rest later, as I think the first line is already a no-go to me, sorry.
While this is a laudable goal, this is not the first time this has been brought up, and very little has changed now compared to before. This would be a MAJOR breaking change for the ecosystem and despite agreeing that opt-in would have been a better default, all it would do is shift the attack to the next easiest way to compromise the supply chain (post-install
scripts currently being the easiest right now in the npm
ecosystem). There are no easy fix-all's when it comes to running third-party code, and I am still not sold on the juice being worth the squeeze on this one.
IIRC, the last time this was brought up it was also brought up that the current config is a "all or nothing" thing (although I seem to recall some changes to that recently, so I might be wrong now). IMO, a much better starting point for this discussion would be incremental improvements which would not break the entire ecosystem all at once. I have not been following the RFC calls or PR's much recently, but I am sure someone who has could point to a few others in this space which have been discussed.
I am proposing "2FA or nothing" thing instead, which seems way more sensible to me, still as breaking change.
Can anyone please read my suggestion as a long time needed extra, not just as a blocker? Thank you!
I am proposing "2FA or nothing" thing instead, which seems way more sensible to me, still as breaking change.
Have you opened an RFC for this? I would love to see a --require-2fa
config which would fail install if any packages were not published with 2FA as long as it had a good set of tools for ignoring things. Seems like a better way to respond to a credential theft as is the source of these recent issues.
A breaking change like this in npm needs to take into account all use cases to date (Electron based Apps too), not just CI, but I'll read the rest later, as I think the first line is already a no-go to me, sorry.
That's certainly unfortunate, considering that had you read the RFC before this, it explicitly discusses a strategy of having a warning transition period to give everyone time to update (Electron apps can trivially take this time to add the flag and avoid any hiccups whatsoever, completely transparently to their users. I'm also not even sure this is an issue since Electron apps can ship "built" and don't have to necessarily themselves npm install on users' machines, but again, that would itself be mitigated completely with a one-line change). Anyways, your goal of showing me disrespect has succeeded, by declaring that a huge post that tried to in good faith address your comments isn't even worth reading since you didn't like the opening sentence for a reason that was covered in the original RFC. This however does not prevent you from continuing to criticize.
Hot button issues with folks passionate in the ecosystem can get tough to navigate, but I recommend taking a moment to listen. There will always be someone who disagrees in any sufficiently populated OSS community, and that is a good thing. In this case, I think this issue has made SOOO many rounds, and this solution has been recommended and talked through before, that it might lead to folks who have been involved in them in the past to jump to conclusions.
I think we can all agree that you did a great job writing up the RFC @tolmasky, and I highly doubt that going back in time and being able to change things before it was so entrenched, anyone would disagree with you. I would recommend letting folks comment on the RFC, and then attending the next RFC call. The folks who regularly attend are great, and really open to discussion.
Install scripts that can run just about anything by default pose some pretty serious security considerations
Since this presupposes that the package you're installing might have malicious code, how do you propose to handle the threat posed by running your own trusted code which import
s from these untrusted libraries?
While this is a laudable goal, this is not the first time this has been brought up, and very little has changed now compared to before. This would be a MAJOR breaking change for the ecosystem and despite agreeing that opt-in would have been a better default, all it would do is shift the attack to the next easiest way to compromise the supply chain (post-install scripts currently being the easiest right now in the npm ecosystem). There are no easy fix-all's when it comes to running third-party code, and I am still not sold on the juice being worth the squeeze on this one.
A number of things here, hopefully some of which ew at RunKit can be helpful with. We have to regularly analyze the usage of install
scripts on RunKit, since we try to install every combination of every version of every package on npm, specially if we can join forces with npm's side of the data. I feel fairly confident that this can be done in a non "break the world" fashion, especially because I think many of the remedies can be handled by authors vs. the developers using the packages. So, in particular:
- We should of course no matter what have a transition period where npm simply warns about install scripts.
- I think there are also some "common sense" user-driven heuristics that could mitigate many of the problems completely. For example, if a
package-lock.json
is present then you can don't need to require the flag. This is because there is no danger of picking up a random patch update in this case, you are only installing items you've already installed in the past, and even checking against the checksum. If you wanted to be super safe, you could only apply this rule for package-lock.jsons generated before the "opt-in" transition (this can be determined by the presence of a property in the package lock) This means that you are truly catching the most egregious cases of installing unlocked packages that may pick up unintended script additions. - I think we could fairly reasonably establish the "affected radius" of users by combining download data and dependency-chain info. We could thus establish a fairly good metric of "safety" to reach before pulling the trigger on anything that would have any actual end-user affects.
- During this period we can assist package authors in getting their packages updated. We've also done a lot of work in trying to group "install script usage", so we could fold in some "declarative" alternatives for a lot of the "piggy-backing off of install just because its the only way to do it" use cases. For example:
- Providing an official way to ask for donations, so that doesn't have to go through install (I can't remember if this was already possible, apologies if this is already the case, but if so, then certainly letting people know that if in a year they don't switch to the official declarative support.md format or whatever their packages will require an install flag will probably go a long way to getting everyone transitioned).
- Providing a system for declaring and installing "outside of npm" dependencies, either as a check, or as an install. We've actually implemented this ourselves for our own packages internally, something like "aptDependencies" and "aptRequirements", where npm is responsible for either making sure you have certain dependencies or fetching them. This would also just be a great ecosystem move to further "surface" the dependency structure of npm, and make it way easier for people to convey that their packages require certain tools to be available, etc.
With regard to the "bar" we're trying to reach with this strategy, I think the goal is of course not to fix everything, which isn't realistically possible. I do however think we can make reasonable progress to make "passive" attacks considerably harder to execute (where packages don't even need to be run for an attack to be successful).
Seems like a lot of work for the entire ecosystem when all malicious authors would have to do is just switch to running malware on first run via require/import instead which would probably happen as soon as this was implemented.
Perhaps a flag instead to refuse to install packages that have been published in a recent time period or at least flag them interactively? That way each individual user or organisation could configure their preference on how long packages have to have been in the wild before they even get downloaded onto their system.
Very supportive of this proposal. .*(install|prepare) scripts not only decrease security, they slow down installations, complicate caching and remove reproducibility by introducing side effects, and pose challenges for cross-platform compatibility.
These scripts can't be part of the "happy path" of npm. They need to be painful to use and consume and phased out of the default path of installation.
I thoroughly agree with the sentiment that the ecosystem having developed an excellent alternative for installing pre-built binary addons targeting multiple platforms means "this time is different" (I'm cognizant this has been proposed many times before)
For example, if a package-lock.json is present then you can don't need to require the flag. This is because there is no danger of picking up a random patch update in this case, you are only installing items you've already installed in the past, and even checking against the checksum.
I like this idea. I think defaulting to trusting the scripts when a package-lock is present and not trusting them when it's absent is sensible.
@tolmasky I'd also encourage you to file a proposal with yarn and pnpm. npm has been conservative about accepting innovations unless they're already popularized by competing package managers.
Since this presupposes that the package you're installing might have malicious code, how do you propose to handle the threat posed by running your own trusted code which imports from these untrusted libraries?
This is a great question and I touched on it briefly in the response above, but I think it's worth addressing clearly, if only to establish expectations: this isn't meant to target those threats (but there are other ideas out there that do!). The goal of this RFC is explicitly to handle a unique class of "passive" threats that exist by simply being present in the dependency chain. This can make them particularly nefarious precisely because they don't need to be imported or run. This fact can make them easier to sneak into codebases (since people often don't review "metadata" files as thoroughly as code files, so the addition of an innocuous package may not raise any eyebrows, or the even harder to find two step approach of initially getting a "safe" package in, only to then modify its internal dependencies afterwards), and can also make these attacks "resistant" against mitigations targeting imported code, since something like code isolation won't help you if the problem is happening during the install. Ultimately, these different kinds of attacks will require different strategies, but I think the install script
trick has made the barrier to entry to create these kinds of hard-to-spot "passive" attacks very low.
I think this is a great proposal and have no real issue with it. But, an alternative that would move the burden from users back to NPM is that packages with install must be reviewed and approved by NPM for every publish and probably require an additional cost for said review and 2FA.
I like this proposal more, but if people really don't want breaking changes, this would not cause any breaking changes for end users and motivate publishers to use alternative methods... Just a thought
I'm not sure I agree with the "next goal post" thinking as a reason to stay put. We do not have an easy way to evaluate package contents without installing them (side note: something to fetch the tarball without installing would be super useful as well). Yes, you can run every install with --ignore-scripts, but that's crap DX. So yeah, the next goal post does exist, but that doesn't mean we need to continue suffering and preventing actual improvements in the process for people who do care.
A blanket "always ignore install scripts" as opposed to the currently available "always ignore all scripts" (which does not allow anything npm run, last I checked) would be useful.
It doesn't have to start as opt-in. A more comfortable opt-out would be better than what we have now.
And yes, 2fa would be nice. I wish we could revive the staging discussions (#92, one approach for 2fa for automation). I also have https://github.com/nodejs/package-maintenance/pull/282 parked for a very long time.
Not going to hold my breath, though, see you back here in a year. Notice how it's always around budget planning season that these things are more common? Maybe someday the budget for securing things will actually happen...
The goal of this RFC is explicitly to handle a unique class of "passive" threats that exist by simply being present in the dependency chain. This can make them particularly nefarious precisely because they don't need to be imported or run. ... This fact can make them easier to sneak into codebases (since people often don't review "metadata" files as thoroughly as code files,...
I see. If I understand correctly, the attack vector here is code which is contributed by an attacker who does not have commit rights to repo or publishing rights to the package, and then the contribution is merged by a maintainer.
Do you have any recent examples of this so it's easier to understand how such an attack might be carried out? In the examples you have given (coa, rc) the attacker had access to package developer's account (publishing rights).
A slightly biased/tangential anecdote: PHP's composer
went the other way from the start. They never allowed library-packages to run their own installation steps. (Only root-level/consumer-packages could define install steps.) It was a recurring request for a few years (eg https://github.com/composer/composer/issues/1193), and (from my POV, as a downstream dev) I felt it held composer
back from doing some useful things. But I do appreciate that it reduces the attack-surface and has probably discouraged some supply-chain attacks.
Similar proposals were raised to allow installation logic with some kind of whitelist. The ship for that probably sailed a few years ago, but... eventually (late last year) I implemented a version of it. So you can see an example of such behavior here:
For installers/upgraders, it did introduce an extra prompt. I was a little concerned about potential for user-pushback. (Available signals suggest ~1000 of our deployments do composer
-based administration.) So far nobody has complained about the extra workflow step being onerous.
@totten Doesn't that have the same problem as Windows' UAC prompt -- every single user just clicks yes
without a second's thought. Appreciate composer
has more options that just yes
and no
but I think the point still applies.
Hey all. Myles from the npm team here. I've been trying to think through how we get to a place where we can disable install / postinstall scripts from running by default.
The biggest gap imho, as others have pointed out, is lazy loading platform specific binary assets. This is something I think we should figure out how to support in the client in a static and consistent manner.
I think with that behavior in place we could safely change defaults in a Semver Major.
I'm just stepping away for the weekend and plan to come back to this when back in the office next week.
It's exciting to see all the momentum for this type of change, even just 2 years ago I don't think we'd see such wide support for such a large breaking change
Wouldn't signing npms on publish with 2FA protected allowed public keys solve this class of problems?
Doesn't that have the same problem as Windows' UAC prompt -- every single user just clicks yes without a second's thought. Appreciate composer has more options that just yes and no but I think the point still applies.
Good point. I don't have data for a systemic answer. Personally, I find it analogous to TOFU in SSH. During the first invocation, you don't really know what to expect, and many folks would blindly accept the suggestion. But then the message goes away (decision is saved into JSON); for most day-to-day tasks (git pull && composer install
or git clone && composer install
or even composer update
), the validation is invisible. The message should only pop-up if something changes, eg
- (SSH) If a host starts advertising a different public-key, then that is suspicious. The client prompts for confirmation.
- (Package manager) If a package wants to increase its permissions, then that is suspicious. The client prompts for confirmation.
So trying to apply to the rc
example - any downstreams newly adopting rc
@1.2.9 would be liable to blind-acceptance. But all existing downstream upgrading from rc
@1.2.8=>1.2.9 would get an unusual prompt.
Perhaps your question ultimately turns on "how frequently do SSH hosts change their keys" or "how frequently do packages try to increase their permissions"?
For those who mentioned 2FA, there is still an issue with the automated publishing process that would usually use an access token. A token can be leaked, thus 2FA does not solve everything.
Perhaps a flag instead to refuse to install packages that have been published in a recent time period or at least flag them interactively? That way each individual user or organisation could configure their preference on how long packages have to have been in the wild before they even get downloaded onto their system.
I am not sure I understand how this would solve this issue? Wouldn't that mean that in case of (critical) security patches everybody using this flag would've to disable it or else run insecure software? Another question I am asking myself here is: if this flag is opt-in instead of opt-out β¦ the attack surface of install scripts isn't really reduced because a lot of people wouldn't know about the flag or use it. Additionally, if everyone waited for say, a week, before installing it would technically increase the window of time in which malicious scripts etc. can be found. But by whom? Who'd do the auditing of this? I currently imagine this would only postpone the inevitable and recreate a status quo "7 days later" (using the example wait window).
I am a casual user of npm/yarn and wouldn't like this burden shifted to me if a better alternative (e.g. like SSH TOFU as previously mentioned) appears to be feasible. 2FA publishing also sounds like a good idea to me, however, as an additional improvement not as an alternative to requiring explicit consent for install scripts by default.
What if npm maintains an anti-malware database, just like a regular antivirus? Any install can then be directly halted if the package is marked as unsafe. To make it a non breaking change, anti-malware can be used as a separate package. npm can issue a warning if installation is triggered without this anti-malware package. Regular definition updates headache can be removed if the database is maintained online.
A single source of truth for all the vulnerabilities. This can help notify people via tweet/mail about recent alerts. Organisations can ping the database regularly to find if the package is vulnerable or not.
To make it more streamlined, a simple set of rules like 2FA publish, author's information, last publish source can be kept.
@tolmasky
your goal of showing me disrespect has succeeded
quite the opposite, I wanted to clarify eventually one point after the other, no disrespect meant, not sure where you got that
by declaring that a huge post that tried to in good faith address your comments isn't even worth reading
I've never said that, I explicitly wrote "I'll read the rest later" and I tell you why: I am in another country, on data roaming, due my father in law (the only one I have) at the hospital due lung cancer complications ... I was trying to read through, but had no time yesterday to come after the entirety of your post, so I've tried to break points down, and promised myself to come back after taking the time, in a place with internet, to read and reply.
Your answer now made it pointless for me to read through for real though, so I hope this conversation keeps going without shaming other developers intentions out of the blue, and with such ease, thank you.
What's not been discussed here yet is the relationship between what we could do and the actual reason we're talking about it.
What we need is not banning ALL postinstall scripts. We need to ban UNEXPECTED postinstall scripts.
I tried to remedy this with my recent work - check here https://dev.to/naugtur/get-safe-and-remain-productive-with-can-i-ignore-scripts-2ddc
If we want to address this on the NPM level, the biggest result we can get without breaking much is preventing packages from introducing postinstall scripts.
So if you npm install something with a script and there's no package lock, you get asked if you trust it. When lock exists, it should denote if a package has a script to run and if it says not and the latest version has a postinstall script, raise the alarm. Effectively make a built-in allow-scripts that propagates automatically.
There's very little popular packages with existing postinstall (I know because I have spent a few nights scanning npm for those) and taking over one of them instead of one of countless others is a totally different order of magnitude in probability.
None of the large impact attacks we saw would be possible with this in place.
TL;DR: First eliminate unexpected scripts showing up all of a sudden instead of all.
I was trying to read through, but had no time yesterday to come after the entirety of your post, so I've tried to break points down, and promised myself to come back after taking the time, in a place with internet, to read and reply.
I do apologize if there was a misunderstanding, but in one of your (many) replies to me on Twitter you pointed me to your reply by saying "I disagreed already with the first line you wrote, I'll read the rest once that's clarified", which at least to me seems to pretty clearly state that you expected me to address your concern with the first sentence of my post as a precondition to reading any more of it, so I don't think my interpretation was really that off base.
That tweet thanked you, and it was rushed too. I donβt have much more to add.
A couple quick updates from compiling questions and comments both here and HN. I'll try to incorporate these into the actual RFC but want to get some notes down here sooner rather than later:
-
I'm trying to compile a list of the various scenarios in which install scripts can be exploited and add those all to the "motivation" section.
-
I think there may be a good argument for adding an entry in the
package.json
file when you use--allow-scripts
, as someone could otherwise just manually add an entry in thepackage-lock.json
, which often goes undetected in PRs since GitHub will hide the diff of largepackage-lock.json
changes, and few people actually read those multi-hundred line changes carefully.