>When you run npm install, npm doesn't just download packages. It executes code. Specifically, it runs lifecycle scripts defined in package.json - preinstall, install, and postinstall hooks.
What's the legitimate use case for a package install being allowed to run arbitrary commands on your computer?
> What's the legitimate use case for a package install being allowed to run arbitrary commands on your computer?
The paradigm itself has been in package managers since DEB and RPM were invented (maybe even Solaris packages before that? it's been a minute since I've Sun'd); it's not the same as NPM, a more direct comparison is the Arch Linux AUR - and the AUR has been attacked to try and inject malware all year (2025) just like NPM. As of the 26th (5 days ago) uploads are disabled as they get DDOSed again. https://status.archlinux.org/ (spirit: it's the design being attacked, pain shared between AUR and NPM as completely disparate projects).
More directly to an example: the Linux kernel package. Every Linux distribution I'm aware of runs a kernel package post-install script which runs arbitrary (sic) commands on your system. This is, of course, to rebuild any DKMS modules, build a new initrd / initramfs, update GRUB bootloader entries, etc. These actions are unique outputs to the destination system and can't be packaged.
I have no data in front of me, but I'd speculate 70% (?) of DEB/RPM/PKG packages include pre / post install/uninstall scriptlets, it's very common in the space and you see it everywhere in use.
There's nothing inherently wrong with that. The problem is npm allows any random person to upload packages. It's completely untrusted. Contrast that with Linux distributions which have actual maintainers who take responsibility for their packages. They don't generally allow malware to make it into the official software repositories. In many cases they went as far as meeting each other in person just to set up a decentralized root of trust with PGP. It's so much better and more trustworthy.
The truth is npm, pip, rubygems, cargo and all the other programming language package managers are just fancier versions of the silly installation instructions you often find in README files that tell people to curl some script and pipe it into bash.
Even when random people are allowed to contribute (AUR, PPAs) there seems to be more scrutiny and fewer incidents. Possibly because they are secondary to the main repos and possibly because the people who use them are made aware of the risks.
NPM etc. are a bit like Arch would be if everything was in AUR.
With PPAs or COPR (or third party repositories, e.g. slack) you have to explicitly enable each repository too. Even if a repository is enabled by package you are prompted the first time a new repository is used (actually I don't know if that's true when gpgcheck is disabled? If not this would be an easy fix in apt/dnf)
Easy example that I know of: the Mediasoup project is a library written in C++ for streaming video over the internet. It is published as a Node package and offers a JS API. Upon installing, it would just download the appropriate C++ sources and compile them on the spot. The project maintainers wanted to write code, not manage precompiled builds, so that was the most logical way of installing it. Note that a while ago they ended up adding downloadable builds for the most common platforms, but for anything else the expectation still was (and is, I guess) to build sources at install time.
Believe such build tools and processes should be run inside a container environment. Maybe once all OS have native, cheap and lean containers and permit dead simple container execution of scripts, this will be possible.
People want package managers to do that for them. As much as I think it's often a mistake (if your stuff requires more than expanding archives different folders to install, then somewhere in the stack something has gone quite wrong), I will concede that because we live in an imperfect world, other folks will want the possibility to "just run the thing automatically to get it done." I hope we can get to a world where such hooks are no longer required one day.
Hard. In npm land you install React and 900 other dependencies come with it. And how ok are you reviewing every single one of those scripts and manually running them? Not that it is good that this happens but realistically most people would just say “run all” and let it run instead of running each lifecycle script by hand.
My solution is to bypass React entirely. I'd much rather have a smaller, possibly less functional or pretty front end than to have to worry about this stuff continuously. I would not get any work done. There is no way I'm going to take on board 900 dependencies which I will then inflict on the visitors to my website.
I am people and I don’t want to download 900 dependencies. But if I want to use React or Vue or anything but the most trivial JS libraries or frameworks I am held hostage by the fact that their dependencies have dependencies.
Surely there are ways to just stick to absolute basics and you can get quite far with what is built in or some very small libraries, but I guess it depends on where you would be most productive.
rpm and dpkg both provide mechanisms to run scripts on user machines (usually used to configure users and groups on the user machine), so this aspect is not an NPM-specific. Rust has the same thing with build.rs (which is necessary to find shared C libraries for crates that link with them) so there is a legitimate need for this that would be hard to eliminate.
Personally, I think the issue is that it is too easy to create packages that people can then pull too easily. rpm and dpkg are annoying to write for most people and require some kind of (at least cursory) review before they can be installed on user's systems from the default repos. Both of these act as barriers against the kinds of lazy attacks we've seen in the past few months. Of course, no language package registry has the bandwidth to do that work, so Wild West it is!
rpm and dpkg generally install packages from established repos that vet maintainers. It's not much but having to get one or two other established package authors to vouch for you and having to have some community involvement before you can publish to distro repos is something.
It’s just security theater in the end. You can just as easily put all that stuff in the package files since a package is installed to run code. You have that code then do all the sketchy stuff.
What’s needed is an entitlements system so a package you install doesn’t do runtime stuff like install crypto mining software. Even then…
A package, especially a javascript package, is not necessarily installed to run code, at least not on the machine installing the package. Many packages will only be run in the browser, which is already a fairly safe environment compared to running directly on the machine like lifecycle scripts would.
So preventing lifecycle scripts certainly limits the number of packages that could be exploited to get access to the installing machine. It's common for javascript apps to have hundreds of dependencies, but only a handful of them will ever actually run as code on the machine that installed them.
True… I do a lot of server or universal code. But don’t trust browser code either. Could be connecting to MetaMask and stealing crypto, running mining software, or injecting ads.
And with node you get files and the ability run arbitrary code on arbitrary processes.
I would expect to be able to download a package and then inspect the code before I decide to import/run any of the package files. But npm by default will run arbitrary code in the package before developers have a chance to inspect it, which can be very surprising and dangerous.
PHP composer does the same, in config.allow-plugins.<package> in composer.json. The default behavior is to prompt, with an "always" option to add the entry to composer.json. It's baffling that npm and yarn just let the scripts run with nary a peep.
Finally sane solution. When I was thinking about package manager design, I also thought that there should be no scripts, the package manager should just download files and that's all.
The objection is to the redundant, flowery prose overall, and the overall inaccuracy. (Of course the installer "doesn't just download packages"; installation at minimum would also involve unpacking the archive and putting the files in the right place....)
In about as much text, we could explain far better why and how NPM's behaviour is risky:
> When you install a package using `npm install`, NPM may also run arbitrary code from the package, from multiple hook scripts specified in `package.json`, before you can even audit the code.
A) maintainers don’t know any better and connect things with string and gum until it most works and ship it
B) people who are smart, but naive and think it will be different this time
C) package manager creators who think they’re creating something that hasn’t been done before, don’t look at prior art or failures, and fall into all of the same holes literally every other package manager has fallen into and will continue to fall into because no one in this industry learns anything.
Many front end tools are written in a faster language. For example, the next version of TypeScript compiler, SASS, SWC (minifier), esbuild (bundler used by Vite), Biome (formatter and linter), Oxc (linter, formatter and minifier), Turbopack (bundler), dprint (formatter), etc.
They use proinstall script to fetch pre-built binaries, or compile from source if your environment isn't directly supported.
Notable times this has bitten me include compiling image compression tools for gulp and older versions of sass, oh and a memorable one with openssl. Downloading a npm package should ideally not also require messing around with c compilation tools.
What's the legitimate use case for a package install being allowed to run arbitrary commands on your computer?
Quote is from the researchers report https://www.koi.ai/blog/phantomraven-npm-malware-hidden-in-i...
edit: I was thinking of this other case that spawned terminals, but the question stands: https://socket.dev/blog/10-npm-typosquatted-packages-deploy-...