[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)