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