7 Tips To Avoid Being Hacked (npm, pnpm & bun)

BBetter Stack
컴퓨터/소프트웨어AI/미래기술

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.

Key Takeaway

Securing JavaScript projects against supply chain attacks requires a combination of automated dependency auditing, restrictive package manager configurations, and disciplined version control practices.

Highlights

  • Configuring release age gating in package managers like NPM, PNPM, and Bun prevents installation of newly published, potentially malicious packages.

  • Disabling automated install scripts or using allow-lists for specific packages blocks the most common vector for supply chain attacks.

  • Tools like Socket Firewall and MPQ audit dependencies before installation by checking for vulnerabilities, typo-squatting, and malicious behavior.

  • Using 'npm ci' or equivalent commands with frozen lockfiles in CI/CD environments prevents unauthorized version changes during production builds.

  • Restricting git-based dependencies and exotic sources limits attack surfaces that bypass standard registry verification.

  • Pinning dependencies in package.json and reducing the total number of third-party packages minimizes exposure to compromised sub-dependencies.

Timeline

Configuring Package Manager Defenses

  • Release age gating delays installation of new packages, allowing time for supply chain attacks to be detected.
  • Disabling automatic lifecycle install scripts prevents malicious code execution immediately upon package installation.
  • Restricting exotic git-based dependencies blocks attackers from bypassing registry integrity checks.

NPM, PNPM, and Bun all support release age gating to avoid brand-new packages. PNPM 11 defaults this to one day, while other managers allow custom configurations in their respective config files. Turning off install scripts is essential, as most attacks use them to run code on a local machine. For packages requiring legitimate scripts, allow-lists should be utilized to maintain security while enabling necessary build tools.

Automated Security Auditing

  • MPQ scans for vulnerabilities, typo-squatting, and suspicious maintainer information before package installation.
  • Socket Firewall blocks human-confirmed malicious packages and warns about AI-detected threats across JavaScript, Python, and Rust.
  • Clearing package manager caches ensures that all previously installed packages are verified by these security tools.

Security tools like MPQ and Socket Firewall act as proxies for standard installation commands. They provide real-time auditing by cross-referencing packages against databases like Snyk and checking for signs of malicious activity. These tools can identify risks like typo-squatting, where names mimic popular packages to deceive developers.

Lockfile Integrity and Deployment Practices

  • Reviewing lockfiles in pull requests prevents attackers from swapping resolved URLs for malicious packages.
  • Using clean install commands like 'npm ci' or 'pnpm install --frozen-lockfile' ensures the environment matches the lockfile exactly.
  • Committing lockfiles to version control is required for integrity verification.

Lockfiles contain the specific download URLs for dependencies, making them prime targets for tampering. Tools such as lockfileLint help validate these files to ensure all packages originate from trusted sources and match expected integrity hashes. CI/CD pipelines must strictly use clean install commands to prevent the automatic installation of unverified or swapped dependency versions.

Development Habits for Risk Reduction

  • Avoiding indiscriminate updates of all dependencies at once limits exposure to new, untested code.
  • Reducing the number of total dependencies lowers the overall attack surface of a project.
  • Pinning dependencies to exact versions ensures that upgrades remain deliberate and controlled choices.

Practical habits are as important as configuration changes. Replacing heavy dependencies like Lodash or Axios with native code or small snippets reduces unnecessary risks. Treating every update as a security-sensitive event rather than a routine task significantly hardens a project against automated supply chain compromises.

Community Posts

No posts yet. Be the first to write about this video!

Write about this video