Transcript
00:00:00If you install NPM packages, you're a target. Maybe not today, maybe not this week, but it's
00:00:05definitely coming. We have had hundreds of packages compromised in the last few months alone,
00:00:09and it is not slowing down. So instead of worrying every time one of these has come up,
00:00:14I actually went and locked down my own setup across NPM, PMPM, and BUN, and it turns out most
00:00:18of the stuff that would have saved you takes about 30 seconds to set up. So in this video,
00:00:23I'll take you through the seven changes that I made from a single line of config change that
00:00:27kills the most common attack outright, to free tools that the attackers themselves submitted
00:00:30would catch them before the package even reached your machine. Let's get into it.
00:00:39The first and most simple one is just stop downloading new packages. Most of these supply
00:00:44chain attacks get caught within hours, so if you just don't install brand new versions the second
00:00:48they drop, you actually dodge most of these supply chain attacks entirely. All of the major Node.js
00:00:52package managers now offer some form of release age gating. For NPM, you set this in the NPM
00:00:57RC file, and it's set in days. For PMPM, you can set it globally in the PMPM config.yaml file,
00:01:03or you can set this on a per project basis by using the PMPM workspace file, and this value is actually
00:01:08set in minutes. And it's also worth noting that since PMPM 11, this actually has a default to one day,
00:01:14so even if you don't set it, you still have some protection. For BUN, you set this in the
00:01:17bumfig.toml file, either globally or again per project, and this one is actually set in seconds.
00:01:23It really amazes me that across three package managers for the same setting, we have landed on
00:01:27three different values, and it kind of sums up this ecosystem quite well. Once you have this set,
00:01:32if you then install the package like tanstack's react start one, you can see it doesn't install the
00:01:36version, it actually installs the most recent one, which fits my release age criteria, which for me
00:01:41was one week ago. Now if you ever need to bypass this, perhaps there's been a security issue with the
00:01:45package that you're installing, and you need to install the latest version, you can still do so
00:01:49from the command line, but do watch out for LLMs here as well, as I've actually seen cases where LLMs
00:01:54just use this to bypass and install the latest versions anyway. Another thing to keep in mind is
00:01:59that MPX and BUNX don't respect this setting, they actually still install the latest version,
00:02:03and there is an open PR in BUN to fix this. Now while we're in these settings, let's go ahead
00:02:07and turn off install scripts for NPM as well. PMPM and BUN actually have this behavior by default,
00:02:12so there's nothing to set for them. If you didn't know, when you install a package, that package is
00:02:16actually allowed to run its own code right after it's been installed, and this was done for legitimate
00:02:20use cases like compiling native code or downloading a binary, but the trouble is nearly every single
00:02:26supply chain attack has used this method to run malicious code on your machine right after install.
00:02:30If you do find a package that has a legitimate need for one of these scripts, you can still explicitly
00:02:34enable it. In PMPM, when you install a package that has post-install scripts, it will actually tell you,
00:02:39like esbuild here, and then you can run PMPM approved builds to choose which ones to allow,
00:02:44and this actually sets your PMPM workspace allow builds config, and you can also just use the
00:02:48allow build flag on the install command to do the same thing. For BUN, as I said, it stops these
00:02:52install scripts by default as well, but it's worth knowing that it actually has a curated list of
00:02:56packages that are allowed to run these scripts, and that includes ones that I have installed like
00:03:00esbuild. You can opt out of this by adding trusted dependencies into your package.json,
00:03:04this way only packages you explicitly allow will be able to run post-install scripts.
00:03:09It does also say that if you set the array to empty, that it should override that default list
00:03:12as well, but it doesn't appear to be working for me, and it seems to be a bug that I found on GitHub
00:03:17too. The workaround for this at the moment is just to put one value in that list, and then the default
00:03:21list is ignored. With BUN, you can also run BUN PM untrusted to see which packages want to run
00:03:26scripts but aren't trusted yet, and then you can run BUN PM trust to allow one, or just add it to that
00:03:30trusted dependencies array. You can also entirely disable scripts in BUN by saying install scripts to
00:03:35false in your global BUN fig. For NPM, this is a little more difficult. Honestly, just don't use
00:03:40NPM, use PNPM, but if you really have to use NPM for some reason, you can get a similar allow list
00:03:45behavior by using Lavamote's allow scripts NPM package. This way, it's just an allow list in your
00:03:50package.json as well. The third tip is to block git-based dependencies. With NPM, a dependency can
00:03:55actually be declared as a git url, which bypasses the registry, and can even ship its own NPM RC that
00:04:01re-enables lifecycle scripts. This is actually one of the tricks used in the recent NPM supply chain
00:04:05attack that hit tan stack. You can stop this by setting allow git to none in your NPM config,
00:04:10which blocks them entirely, and the other option is to set this to root, which does allow git-based
00:04:15dependencies to be installed, but only if they're declared in your root package.json, so likely
00:04:19explicitly set by you. For PNPM, this option is block exotic sub-dependencies. When this is set to true,
00:04:25only direct dependencies, so those that you have listed in your root package.json, can use exotic
00:04:29sources such as git repos or direct table URLs. This option doesn't exist in Bun yet though, but there
00:04:35is a PR open to add this, so maybe we'll see it soon. As a bonus to end the tips that involve config
00:04:40changes, PNPM actually has a trust policy setting, which we can set to no downgrade, and this means
00:04:45that PNPM will fail if a package's trust level has decreased compared to its previous release. So if a
00:04:50package was previously published by a trusted publisher, but now it only has provenance or
00:04:55no trust evidence, installation will fail. This should help catch those attacks that find ways to
00:05:00bypass the usual publishing processes. Tip number four though is probably the most powerful one, which is
00:05:04using a tool that actually looks at the packages you're installing and can audit them before they
00:05:08ever touch your machine. For that, we have two powerful and completely free tools. The first one is
00:05:14MPQ. You can set this up by creating an alias for your normal NPM, PNPM, or Bun commands,
00:05:18so that every time you run an install, it actually checks a few things. It will scan for known
00:05:23vulnerabilities against Snyk's database, and flag any package that's less than 22 days old. It will
00:05:28catch typo squatting, which is where someone's published a package like express with 1s, hoping
00:05:32that you'll make a typo and install that one instead. It will verify the registry signature and
00:05:37build provenance. It will warn you when a package has pre and post install scripts, and on top of all of
00:05:41that, it checks for basic things like the download count, if there's a readme, a license, a repo URL,
00:05:46and what the maintainer's email is, and if that domain is even still registered. It does all this
00:05:51and then gives you an interactive report on your packages, where you can still decide whether you
00:05:55want to install it. The second one is socket firewall. This is actually the one that I use,
00:05:59and again, you alias this for all of your usual package managers, and this one actually supports
00:06:03ones outside of JavaScript too, like Python and Rust. Similar to MPQ, when you run an install with
00:06:08this, it will check socket and block any human confirmed malicious packages, and it will also warn
00:06:12you on any AI detected threats, which are ones that haven't been human reviewed yet. To be honest,
00:06:16I mainly chose socket firewall as it was the one I heard of first, but I also trust socket as they've
00:06:21caught a lot of the recent malicious packages, and the attackers even did an interview where they said
00:06:25that socket will detect the malware before the package even reaches your machine, which is a pretty good
00:06:30advert for them, and I also like that it supports more than just JavaScript with UV pip and cargo.
00:06:35If you do install either of these, you'll want to make sure that you clear your package manager's
00:06:38cache just to make sure that every installed package is checked by the firewall. Tip five is about your
00:06:42lock files. Your lock files are where the actual download URL for every package lives, and the
00:06:47problem is, pretty much nobody reviews their lock files in a PR. So if someone opens a PR against your
00:06:51repo, they can quietly edit a resolved URL to point to a package that they control, and also set the
00:06:56matching integrity hash so that nothing looks off. Now the next time you run an install, you're pulling
00:07:01the code from the attacker's server. Good news is though, if you're using PMPM, you're not vulnerable
00:07:05to this one. It doesn't keep those table sources that can be swapped out, and it also won't install
00:07:09anything that's in the lock file, but not actually declared in your package.json. I couldn't find any
00:07:14concrete information on bun though, so that may still be vulnerable to this. The fix if you're not
00:07:18using PMPM is to use a tool called lockfileLint. You install this as a dev dependency, and it validates
00:07:23your lock file, checking every package resolves from a trusted host, like the NPM registry. It checks
00:07:28the resolved URLs actually match the package's name, and it also checks the integrity hashes are real as
00:07:32well. If it suspects something has been tampered with, it will fail the install. Tip 6 is to stop
00:07:37using NPM install in CI and production, and instead use clean install. The command for this in NPM is
00:07:42just NPM CI, and for bun and PMPM, you actually add the frozen lockfile flag, but they also have CI
00:07:47commands as an alias that do the same thing. PMPM actually uses this by default if it detects it's
00:07:52running in a CI environment. The clean install command installs exactly what's in the lockfile,
00:07:57nothing else, and if the lockfile and package.json don't agree, it just stops and throws an error,
00:08:01instead of guessing and installing anyways. So this should prevent an attacker from sneaking a
00:08:05swapped version in. None of this works though if you're not committing your lockfile in the first
00:08:09place, so do make sure that's in version control and not in your gitignore, and I'll admit when I
00:08:13first started learning to code as a kid, I actually thought you were meant to ignore these, so I'm glad
00:08:17I learned to commit them. So those six tips were mostly config and tooling, but there's also some
00:08:21habits that you can adopt to help you avoid attacks. The first one is to stop blindly updating
00:08:25everything. You may have run NPM update or the other versions of this command before, and just
00:08:30updated every dependency to its latest version in one go, but that's the kind of exact behavior these
00:08:35attackers are hoping for, so actually go through these upgrades and ask yourself why you need to
00:08:39upgrade it. The second habit is becoming more and more relevant, just use fewer packages. Every
00:08:43dependency you add is another attack surface, and most of these attacks spread through dependencies
00:08:48of a dependency, so it's genuinely worth asking why you need that library. Some common examples I see of
00:08:53this are things like Lodash for functions that can be done with a small snippet of code, or Axios when
00:08:58fetch can do the same thing. The reason I say this is becoming more relevant is in the age of agentic
00:09:03coding, it's pretty easy to get AI to write a few functions for you instead of using a dependency.
00:09:08The third habit is pinning dependencies to an exact version, so an upgrade is always a deliberate
00:09:12choice, but just know this actually only locks down the packages that you choose in your package.json,
00:09:17and your dependencies' dependencies can still use their own ranges, which is exactly why that
00:09:21cooldown from tip 1 matters. All of these combined should give you some peace of mind that you won't
00:09:25accidentally install a compromised package. They're not invincible, but they're a lot better than
00:09:29nothing. The ultimate tip is just to run everything in a hardened dev container, but I've always found
00:09:34that to be a bit of a hassle, so I end up falling back to local development. If you know any other way
00:09:38to protect yourself from this kind of attack, let me know in the comments down below, while you're there
00:09:42subscribed, and as always see you in the next one.
Community Posts
No posts yet. Be the first to write about this video!
Write about this video