npm Supply Chain Security: How to Protect Your Project from Malicious Packages
npm Supply Chain Security: How to Protect Your Project from Malicious Packages
In November 2018, a widely used npm package called event-stream was quietly compromised. A new maintainer — who had been granted publish rights after offering to help maintain the project — injected code that targeted a specific Bitcoin wallet application. The malicious code was designed to steal cryptocurrency, and it sat undetected in a package downloaded over two million times per week.
This wasn't a theoretical vulnerability or an academic exercise. It was a real attack that exploited the trust model at the heart of the npm ecosystem. And it was far from the last.
The Trust Problem
npm's greatest strength is also its greatest vulnerability: anyone can publish a package, and anyone can depend on it. This open model has produced an ecosystem of over two million packages, enabling developers to build complex applications at extraordinary speed. But it also means that every npm install is an act of trust — trust in the package author, their operational security, their future intentions, and every maintainer of every transitive dependency in the tree.
A typical Node.js application has between 200 and 1,500 packages in its dependency tree. You've reviewed maybe a dozen of them. The rest are transitive dependencies — packages you've never heard of, maintained by people you've never met, running code on your servers with the same permissions as your application.
Real-World Supply Chain Attacks
Understanding past incidents helps calibrate the threat. These aren't hypothetical scenarios — they happened.
event-stream (2018)
The attack: A new maintainer added a dependency (flatmap-stream) that contained encrypted malicious code. The payload specifically targeted the Copay Bitcoin wallet, attempting to steal private keys. The malicious code was obfuscated and only activated under specific conditions, making it difficult to detect through casual code review.
The lesson: Maintainer handoffs are a critical trust boundary. A package can be perfectly safe for years and become compromised overnight with a single npm publish.
ua-parser-js (2021)
The attack: The maintainer's npm account was hijacked. The attacker published versions 0.7.29, 0.8.0, and 1.0.0 containing a cryptocurrency miner and password-stealing trojan. The package had 7 million weekly downloads at the time and was used by Facebook, Amazon, Microsoft, and Google.
The lesson: Even trusted, widely-used packages maintained by diligent developers can be compromised if the maintainer's credentials are stolen. The package itself wasn't vulnerable — the distribution infrastructure was.
colors.js and faker.js (2022)
The attack: The original maintainer of both packages deliberately sabotaged them. colors.js was updated to print garbled text and enter an infinite loop. faker.js had its code replaced with a sarcastic message. The maintainer was protesting what he saw as exploitation of open-source developers by large corporations.
The lesson: Supply chain risk includes the maintainer themselves acting against users' interests. A package's history of being well-maintained is not a guarantee of its future.
Typosquatting Campaigns (Ongoing)
Attackers routinely publish packages with names similar to popular ones — lodahs instead of lodash, cross-env becoming crossenv. These packages contain malicious install scripts that exfiltrate environment variables (often containing API keys and credentials) to external servers. Dozens of these campaigns are discovered and removed every month, but the window between publication and removal is often long enough to catch victims.
Protestware (2022-present)
Following the colors.js incident, a wave of packages were modified to display political messages, delete files on systems with specific locale settings, or otherwise behave differently based on the user's geography. While not always data-stealing, these incidents demonstrated that any package update can carry unexpected and unwanted behavior changes.
Common Attack Vectors
1. Typosquatting
Publishing packages with names that are slight misspellings of popular packages. Developers who make a typo in npm install or package.json unknowingly pull in the malicious package.
2. Maintainer Account Takeover
Compromising a maintainer's npm credentials through phishing, credential stuffing, or password reuse. The attacker then publishes a new version of a legitimate, trusted package with malicious code injected.
3. Social Engineering Maintainer Access
Offering to help maintain a package, gaining publish rights, then injecting malicious code. This is particularly effective against solo maintainers of popular packages who are overwhelmed with issues and PRs.
4. Malicious Install Scripts
npm allows packages to run arbitrary scripts during installation via preinstall, install, and postinstall hooks in package.json. Malicious packages use these to execute code the moment you run npm install — before you've ever imported or used the package in your own code.
5. Dependency Confusion
Exploiting the resolution order between public and private npm registries. If a company uses internal packages with names that don't exist on the public registry, an attacker can publish a package with that name on the public registry with a higher version number. Some misconfigured setups will pull the public (malicious) version instead of the private one.
6. Star Jacking and Metadata Manipulation
Creating malicious packages that reference popular packages' GitHub repositories, artificially inflating their apparent legitimacy through borrowed star counts and contributor lists.
Practical Mitigations
Lock Your Dependencies
Always commit your lock file (package-lock.json, pnpm-lock.yaml, or yarn.lock). Lock files record the exact versions and integrity hashes of every installed package. Without them, npm install can silently resolve to different (potentially compromised) versions.
Use npm ci instead of npm install in CI environments. npm ci installs exactly what the lock file specifies and fails if there's any discrepancy, preventing unexpected version resolution.
Audit Regularly
Run npm audit as part of your CI pipeline. It checks your dependency tree against the GitHub Advisory Database and reports known vulnerabilities. It won't catch novel attacks, but it surfaces known-bad versions quickly.
For deeper analysis, consider npm audit signatures, which verifies that packages were published through npm's provenance system.
Disable Install Scripts for Untrusted Packages
You can configure npm to ignore install scripts globally:
npm config set ignore-scripts true
This prevents preinstall and postinstall hooks from running. The trade-off is that some legitimate packages (especially those with native bindings) require install scripts to function. A more targeted approach is to use .npmrc with ignore-scripts=true and then explicitly allow scripts for trusted packages.
Use npm Provenance
npm now supports package provenance — cryptographically signed attestations that link a published package to its source code and build process. When available, provenance lets you verify that a package was built from the repository it claims to come from, using a known CI system.
Check for provenance on the npm website (look for the "Provenance" badge) or via the CLI with npm audit signatures.
Pin Exact Versions
Instead of using semver ranges (^1.2.3 or ~1.2.3), consider pinning exact versions (1.2.3) for critical dependencies. This prevents automatic resolution to newer (potentially compromised) versions. The trade-off is that you must explicitly update these dependencies, but that's arguably a feature when security is a concern.
Review New Dependencies Before Adding Them
Before running npm install some-package, spend two minutes checking:
- Download count: Very low downloads on a package that claims to be popular is a red flag.
- Repository link: Does the npm page link to a real, active GitHub repository?
- Maintainer history: Is the maintainer a known, established contributor?
- Package age: Was this published yesterday? Brand-new packages deserve more scrutiny.
- Install scripts: Check
package.jsonforpreinstall,install, orpostinstallscripts.
Use a Private Registry or Proxy
Tools like Verdaccio, Artifactory, or npm Enterprise let you proxy and cache the public registry. This gives you a chokepoint where you can review new packages before they're available to your developers, enforce allow-lists, and maintain availability even if the public registry has issues.
Monitor for Maintainer Changes
When a package changes maintainers, the risk profile changes. Tools that track maintainer changes across your dependency tree can alert you to this critical trust boundary shift.
Minimize Your Dependency Tree
Every dependency is attack surface. Before adding a new package, ask whether you genuinely need it or whether the functionality is simple enough to implement in a few lines of code. Utility packages that wrap trivial operations (the infamous is-odd, left-pad) add real risk for negligible value.
Periodically audit for unused dependencies with tools like depcheck and remove them.
Automated Supply Chain Protection
Manual vigilance is important but doesn't scale. When your dependency tree has hundreds of packages and new versions are published daily, human review becomes a bottleneck.
RepoWarden integrates supply chain security checks into the dependency update workflow. When evaluating a dependency update, it checks for:
- Known vulnerabilities: Cross-referenced against advisory databases, blocking updates to versions with reported CVEs.
- Maintainer changes: Flags when a package has changed maintainers since your currently installed version, alerting you to the trust boundary shift that enabled attacks like event-stream.
- Install script changes: Detects when an update introduces new
preinstall,install, orpostinstallscripts — the most common vector for immediate code execution attacks. - Suspicious behavioral changes: Analyzes whether an update introduces unexpected new capabilities like network access, filesystem operations, or environment variable reads that weren't present in previous versions.
These checks run automatically as part of RepoWarden's test-first update process. Instead of merging an update and discovering the problem later, potentially compromised updates are flagged before they enter your codebase.
Security is a Process, Not a Product
No single tool eliminates supply chain risk. The npm ecosystem's openness is fundamentally at tension with security guarantees. But layered defenses — locked dependencies, regular audits, minimal dependency trees, install script controls, and automated monitoring — significantly reduce the attack surface.
The attacks described in this post succeeded because they exploited gaps in routine processes: maintainer credentials left unprotected, install scripts that ran unchecked, updates that were merged without review. Closing those gaps doesn't require heroic effort. It requires consistent application of basic hygiene and tooling that makes the right thing the easy thing.
RepoWarden's supply chain checks run automatically on every dependency update. Start for free and add a layer of protection to your update workflow.
See how many engineering hours you'd reclaim
Paste any public GitHub repo. We scan for outdated dependencies, committed secrets, missing CI, weak coverage and more — then estimate the engineering time RepoWarden would save you.
No sign-up required to see the report · Public repos only · Read-only public API
Ready to automate your dependency updates?
RepoWarden keeps your repos secure and up to date — with supply chain protection, automated testing, and clean PRs.
Get started free