[HN Gopher] RFC: Make NPM install scripts opt-in ___________________________________________________________________ RFC: Make NPM install scripts opt-in Author : tolmasky Score : 74 points Date : 2021-11-05 17:44 UTC (5 hours ago) (HTM) web link (github.com) (TXT) w3m dump (github.com) | danenania wrote: | This seems like a step in the right direction for sure, but what | is the threat model here exactly? When would I be concerned about | code in an install script but not in the package itself? | | What we really need are content security policies for node. I | want to define at the top level of my project exactly which file | system directories and internet domains can be accessed, then | have that enforced by the runtime. | johannes1234321 wrote: | the difference is that reviewing code after install is simpler | than before install. | | For review after install I install and fire up my IDE. | | For review before install I have to manually download the | package and figure out the dependency tree and do that for all | packages. | tolmasky wrote: | This is a great question, and there are actually a number of | different reasons: | | 1. Package installation often happens under different | privileges than actually running your end user app. There is | unfortunately a lot of "just use --unsafe-perm to make the | install work" advice out there which means a lot of people are | installing packages as root even though they're not running as | root. Also consider that a lot of npm packages ultimately get | run in the browser, so the attack surface there should have | very little to do with the files on your computer, but install | scripts make this not the case. For the same reason, this means | that this might be the only kind of attack that has the ability | to run on your build machine, since your build machine may not | actually "run" your app. | | 2. The fact that the code doesn't have to be run to be | effective makes it quite difficult to spot, and thus increases | its likelihood of sneaking into your dependency chain. For | example, GitHub will often straight up just hide large | `package-lock.json` file diffs, and no one wants to read | through those. So a very innocuous patch version change in an | otherwise unrelated bug fix could completely fool the PR | reviewer: this is because there's no code "history" that would | imply any sort of entryway into attack. All the code looks | totally reasonable. By having to explicitly allow the package | to use install scripts however, all of a sudden the PR would | contain a clear indication that new foreign code that isn't | represented anywhere in the commit will be run. This is huge. | | 3. This also takes advantage of the fact that the vast majority | of npm installs aren't done by humans, but by CI and production | deploys. Again, by ensuring that the attack is completely | "passive", that is to say, as long as you get installed you've | succeeded. A lot of times this can be triggered just by issuing | a PR with automatic build and CI machines. The correct behavior | for something trying to run a new script on your machine when a | PR is submitted isn't to infect your CI machine, it should be | for the test to fail with "package X tried to use an install | script". | danenania wrote: | All great points, thanks. It would definitely reduce the | surface area for attacks. In practice, it may just change | which kinds of packages are targeted (packages in the build- | time dependency tree instead of runtime), but it would still | be a win. | | Ultimately, I still think we need real sandboxing built in to | the runtime to _really_ solve this problem. Is there | something inherent to node 's architecture that would make | this impossible or is it just a ton of work? Could Chrome's | sandbox and CSP implementation be piggy-backed on perhaps? | Deno takes some steps in this direction but they stopped well | short of what's really needed. | Groxx wrote: | It boggles my mind that languages and package managers do not | support ACLs for libraries. | | leftpad has no need of install scripts, nor `eval`, reflection, | or access to my disk or the network. _nor should it be allowed to | gain them in the future_ , at least without a million alarm bells | ringing and explicit approval. | | ACLs would allow establishing "moats" of dramatically-more- | difficult-to-attack libraries, and encourage libraries to | _voluntarily_ reduce their attackable surface to make them more | likely to be approved. If leftpad can 't touch anything except | what I give it, using leftpad in a practical attack requires | control over at least two libraries, or seriously problematic | coding patterns (like `eval "var = leftpad(var)"`). | SahAssar wrote: | I don't think this would have much of an impact since it would | only require the attacker to change the injection point from the | install to swapping out the "main" entry in the package json to a | compromised file, right? | | I think the problems of npm and the js world in general is the | depth and breadth of the dependency trees and the | misunderstanding of transitive dependencies. I've heard devs say | that they only have a single dependency when using CRA (which | IIRC pulls in 1500ish transitive dependencies). I've heard devs | say that a dependency is always better (even if it replaces a | single line of code) than your own code. | | Besides that even simple dependencies seem to be updated even | when they were "done". Many devs see a repo that has not had a | commit for a few months and consider the project dead instead of | done, so there is an incentive to keep updating things that | didn't need updating or for dependents to switch to "actively | developed" projects for their dependencies. | | So yes, requiring 2fa would be great, making the install steps | not able to run arbitrary code would be great, and most of all | requiring repeatable builds from source would be great. I still | think the problem at the core would remain, which is that the | ecosystem is too hooked on excessive dependency usage and sees | newness as a virtue. | smashah wrote: | Package maintainers implement install scripts for a reason. It | should be opt-out, not opt-in. | | There should be granualar control over which packages, and which | scripts you want to block. | | E.g in your package.json: | | `"scriptblock": { "puppeteer" : "*" } ` or ` "scriptblock": { | "puppeteer" : ["preinstall","postinstall"...] } ` | | etc. | axlee wrote: | You have no idea of the packages you are installing. Like | litterally, none, unless you use very narrow, pure-js ones. | d4mi3n wrote: | Sadly security by blacklisting always turns into a game of | whack-as-mole. If there's an unavoidable need for an install | script that may be a case to mature the facilities a package | manager offers rather than requiring packages to run arbitrary | code. | KayL wrote: | default or not, you still run it... I doubt regular developers | will review the whole script before run it. | | how many Linux users copy&paste the command from random webpages | without doubt? It's 100% opt-in. So it won't help at all. | ljm wrote: | IIRC, yarn disables script invocation when installing a package, | and there is a separate mechanism to run them if you need to. I | don't remember the full details of how it works, though. | | Given NPM's increasing viability for large scale supply-chain | attacks, there are other things to worry about still. Perhaps | those other things are more fundamental to NPM's design and can't | be changed overnight. It's still helpful to solve these simpler | problems. | niros_valtos wrote: | This functionality is a must nowadays. To reduce the risk, I | would lock the packages to specific versions and upgrade only | after two weeks or immediately after a critical vulnerability is | fixed. | pictur wrote: | I may be thinking silly but wouldn't it be better if these | dependency reliability issues were fixed by the platform that | released the package? Are there any barriers to publishing a | malicious package via npm right now? | noodlesUK wrote: | I strongly support this. There are of course other ways of | compromising peoples computers when running untrusted code, but | let's get the low hanging fruit. | | I have a very simple vue frontend app that I wrote a few years | ago, and it somehow has >4000 dependencies (including dev | dependencies). The fact that npm install could run code from all | of those (which might not be obvious to a newer dev) is flat out | dangerous. | echelon wrote: | If we're going to develop our software like this (there are no | signs of changing), then we need development environments that | are fully hermetic. | | Production deploys tend to be if you use the right tools. | Docker images, cert signing, ACLs, network policies, etc. But | we have no equivalent for developer machines. And engineers | have access to lots of dangerous things. Docker alone isn't | good enough. | noodlesUK wrote: | I've recently switched from linux to MacOS (now that those | shiny new MacBook Pros exist), and due to this, I've started | using vscode dev containers along with gitpod as my pretty | much exclusive development environments. I did this for | convenience rather than for security, but I must say I felt a | strong feeling of relief when this morning I saw the | advisory, and typed `npm` into my terminal and found that it | wasn't even installed on my host OS. | | I think longer term, we're going to find things like Qubes's | VM for everything model becoming more normalised. | megumax wrote: | That's not really a solution to the problem because the attacker | might change the contents of the package instead of adding | `postinstall` or `preinstall` hooks. | | The more realistic solution would be teams of volunteers that are | auditing the packages and check the differences between specific | versions of those. This doesn't block all possible infected | packages, but most of them, which is better than what we have | now. Everything is based on trust so you can't stop this, but | maybe prevent it. | dane-pgp wrote: | > the attacker might change the contents of the package instead | of adding `postinstall` or `preinstall` hooks. | | Ultimately, any code inside an npm package needs to be run by | default in the context of a sandbox, such as vm2 or SES. That | way a developer would have to opt in to granting permissions | for a package to run executable code. | | https://github.com/patriksimek/vm2 | | https://medium.com/agoric/ses-securing-javascript-in-the-rea... | morelisp wrote: | Ultimately, JavaScript needs to change the culture around its | dependency packaging. | theli0nheart wrote: | Good luck with that. | dane-pgp wrote: | It's sad to see people reject this suggestion based on such weak | grounds. "It won't stop all attacks" is true, but if it raises | the costs of attacks, and protects more people that it harms, | then it is worthwhile. Similarly, "It's a backwards incompatible | change" is not a sufficient argument, as changes like that are | made all the time (of course requiring a major version bump). | | To address the resistance, I would propose a compromise, namely | that install scripts won't be run by default unless the | publishing account is secured by 2FA, or the previous version of | the package also included an install script. That should greatly | reduce the attack surface, and pave the way towards requiring 2FA | for all packages with install scripts as a later step. | [deleted] | ollien wrote: | It raises the cost of attack, sure. That said, just about every | developer I know is not going to think about it and is just | going to hit "OK" at the first opportunity. The VSCode "this | directory isn't trusted" warning comes to mind. I know of no | one who actually takes that to heart. Perhaps we should, but | few will actually take the time, sadly. | dane-pgp wrote: | The default install process should stop and prompt you with | something like: Package ua-parser-js wants | to run a script before installing. The description of | the package is: "Detect Browser, Engine, and Device | type/model from User-Agent data." The reason for the | pre-install script is: "Configuring the local user | agent thing for reasons." This script has been | unchanged since version 0.7.29 which was published: | 14 hours ago The hash of the script is: | 0123456789abcdef Press Y to examine the script, or N | to cancel installation. | | After npm echoes out the script, the user should decide | whether it looks obfuscated or does anything suspicious. If | the user is still unsure, they can search the web for the | hash of the script to see if other people have audited it. | | For automated installs, such as a CI server, there would need | to be a command line argument or config file entry with | something like: allow-preinstall-scripts: | ["0123456789abcdef"] | Arnavion wrote: | >but if it raises the costs of attacks, and protects more | people that it harms, then it is worthwhile. | | But how does it raise the cost of attacks? I don't see why it | would be harder for someone uploading a malicious package to | embed said malicious code in the index.js instead of in the | install scripts. | unilynx wrote: | If I was installing the module only for use in frontend, eg | to be bundled by webpack, the code in your index.js won't | execute on the machine but inside the browser sandbox. | | That makes it a lot harder to steal ssh keys. | Arnavion wrote: | Yes, that's valid, though it isn't about increasing the | cost of attacking (what my comment was about). ___________________________________________________________________ (page generated 2021-11-05 23:00 UTC)