Most npm Supply Chain Attacks Are Preventable if You Change These Defaults
A lot of JavaScript developers treat package installation as a harmless step.
Run npm install, wait a few seconds, move on.
The problem is that modern supply chain attacks are built around that exact assumption.
Malicious packages no longer need complicated exploits. In many cases, all they need is a developer who installs dependencies too quickly, trusts lifecycle scripts by default, or updates packages without checking what changed.
Over the last year, compromised npm packages have become common enough that this is no longer an edge-case security topic. It’s now part of regular development hygiene.
The good news is that most of the protection doesn’t require enterprise tooling or complicated workflows. A few configuration changes across npm, pnpm, or Bun eliminate a large portion of the attack surface immediately.
After hardening my own setup, I realized how much risk comes from defaults that most developers never question.
Delay New Package Releases Before Installing Them
One of the easiest ways to avoid compromised packages is also the least discussed: stop installing brand-new releases the moment they appear.
Most malicious packages get detected fairly quickly. Sometimes within hours. Sometimes within a day.
If your package manager refuses to install packages younger than a certain age, you automatically avoid a large percentage of opportunistic attacks.
All major Node.js package managers now support some form of release-age protection:
- npm supports it through
.npmrc - pnpm supports it in
pnpm-workspace.yaml - Bun supports it in
bunfig.toml
What’s interesting is that each package manager measures the delay differently:
- npm uses days
- pnpm uses minutes
- Bun uses seconds
That inconsistency feels very JavaScript ecosystem.
pnpm actually ships with a one-day protection window by default in newer versions, which is one of several reasons it currently has the strongest security defaults in the ecosystem.
I personally prefer using a longer delay window for production work. Waiting a few days before accepting new releases costs almost nothing, but dramatically reduces exposure to rushed malicious publishes.
If there’s a legitimate emergency fix, you can still bypass the restriction manually.
The important part is making “install latest immediately” the exception instead of the default behavior.
Disable Install Scripts Unless You Explicitly Trust Them
Most supply chain attacks rely on install scripts.
This is the mechanism that allows packages to execute code automatically during installation through hooks like:
postinstallpreinstallprepare
These scripts exist for legitimate reasons. Some packages compile native binaries or download platform-specific dependencies.
But attackers use the exact same mechanism to execute malicious code immediately after installation.
pnpm and Bun already handle this more safely than npm.
With pnpm, packages requesting build scripts are surfaced clearly, and you can explicitly approve them. Bun also blocks many untrusted install scripts by default while maintaining its own trusted dependency list.
npm still behaves far too permissively here.
If you are staying on npm, adding an allowlist system is worth the effort. Otherwise, you are effectively giving every installed dependency permission to run arbitrary code during installation.
That sounds extreme, but that is literally what happens.
The important mindset shift is this:
Package installation is code execution.
Once you view it that way, the defaults stop feeling acceptable.
Block Git Dependencies Unless You Intentionally Need Them
Most developers assume dependencies come from the npm registry.
That’s not always true.
A package can pull dependencies directly from Git repositories or tarball URLs, bypassing normal registry protections entirely.
This becomes especially dangerous because Git-based packages can introduce their own configuration behavior, including lifecycle scripts that may not follow your expected restrictions.
Several recent attacks used this technique to bypass normal trust assumptions.
pnpm already has protection for this through exotic dependency restrictions. npm can also limit Git-based installs to root-level dependencies only.
Bun still lacks strong controls in this area, although support appears to be in progress.
For most teams, the safest approach is simple:
If a dependency is not intentionally declared in your own project, it should not be downloading arbitrary code from Git URLs behind the scenes.
That should never be happening silently.
Add a Package Firewall Before Dependencies Reach Your Machine
Configuration hardening helps, but static rules alone are not enough anymore.
The better approach is adding an inspection layer between your package manager and the internet.
Two tools currently stand out here:
- MPQ
- Socket Firewall
Both work by wrapping your existing install commands and auditing packages before installation completes.
They check for things like:
- known vulnerabilities
- typosquatting attempts
- suspicious install scripts
- invalid signatures
- missing provenance
- fake repositories
- compromised maintainers
Socket has become especially valuable because it actively detects newly published malicious packages very quickly. Some attackers have even publicly acknowledged that Socket catches malware before infection spreads widely.
That says a lot about how effective these scanners have become.
This category of tooling is increasingly important because manual dependency review does not scale anymore.
Modern projects pull hundreds or thousands of transitive dependencies.
No developer is realistically auditing all of that by hand.
Your Lock File Is More Important Than Most Teams Realize
Many developers treat lock files as noisy generated artifacts.
They are not.
Your lock file is effectively a security document.
It defines the exact source and integrity hash for every installed package.
If an attacker modifies those references inside a pull request and nobody reviews the change carefully, your next install may silently fetch malicious code from a different source.
This is one of the reasons pnpm’s architecture is safer by design. It avoids several of the URL-resolution weaknesses that traditional npm lockfiles expose.
If you are using npm, tools like lockfile-lint are worth adding to CI.
They validate:
- allowed registries
- integrity hashes
- resolved package sources
- tampered URLs
Most teams already lint code aggressively.
Very few lint dependency resolution.
That imbalance probably needs to change.
Stop Running Regular Installs in CI
Production environments should not behave like developer laptops.
Using npm install inside CI pipelines allows dependency resolution changes during deployment, which creates unnecessary unpredictability.
Instead:
- use
npm ci - use frozen lockfile installs in pnpm
- use Bun’s equivalent CI install mode
These commands install exactly what the lock file specifies.
Nothing more.
Nothing different.
If the lock file and package definitions disagree, the install fails immediately instead of trying to “help.”
That strictness matters.
Security problems often begin when tooling becomes too forgiving.
Most Dependency Security Problems Start With Developer Habits
Tooling matters, but habits matter more.
A few patterns dramatically increase exposure:
Blind dependency upgrades
Running mass updates without reviewing changes creates unnecessary risk.
Upgrades should be deliberate decisions, not maintenance roulette.
Over-dependency on libraries
Every package increases attack surface.
A surprising number of dependencies solve tiny problems that modern JavaScript already handles natively.
This is becoming even more relevant now that AI-assisted coding can generate small utility functions quickly.
Not every helper needs a package anymore.
Loose version ranges
Exact version pinning reduces unexpected drift.
It does not solve everything — transitive dependencies still matter — but it removes a layer of unpredictability.
Combined with release-age gating, it creates a much more stable dependency environment.
Security Defaults in JavaScript Are Improving, But Slowly
The ecosystem is moving in the right direction.
pnpm has introduced stronger defaults.
Bun blocks many unsafe behaviors automatically.
Security scanners are improving rapidly.
But the JavaScript package ecosystem still fundamentally depends on trust between strangers publishing code at enormous scale.
That reality is unlikely to change anytime soon.
The practical goal is not perfect security.
It’s reducing the number of easy wins available to attackers.
Most supply chain attacks succeed because developers rely on defaults that were designed for convenience years ago, long before this became a serious threat model.
Changing a few of those defaults takes less than an hour.
Recovering from a compromised environment takes considerably longer.