Dependency Security: Supply Chain Attacks via npm
Security First — Part 18 of 30
It's 12:30 AM on April 1, 2026. A developer in a CI/CD pipeline halfway through a routine deploy types npm install. Thirty seconds later, a remote access trojan is quietly running on their machine, phoning home to an attacker-controlled server. Credentials are harvested. SSH keys are exfiltrated. The malware then deletes itself — leaving no obvious trace in the installed packages.
The developer had run npm audit earlier that week. It came back clean.
This wasn't a hypothetical exercise. On March 31, 2026, attackers compromised the npm account of the lead maintainer of axios — one of the most downloaded JavaScript libraries on the planet, with over 100 million weekly downloads. Within 39 minutes of each other, two malicious versions were published: axios@1.14.1 and axios@0.30.4. They were live on the registry for about three hours before npm pulled them.
Three hours. That's all it takes.
Yesterday's article covered dependency scanning tools — npm audit, pip-audit, Snyk, and friends. Those tools are important. But they have a hard limit: they catch known vulnerabilities in known packages. They do almost nothing against supply chain attacks, where the weapon is the package itself, freshly poisoned.
Today we go deeper. We're going to look at how these attacks actually work — the mechanics, the psychology, the real incidents — and what you, a vibe coder building with AI tools, can do to protect yourself beyond just running audit commands.
Three Ways Attackers Get Into Your node_modules
1. Typosquatting: The Fat-Finger Trap
Imagine you're asking an AI coding assistant to add an HTTP client to your project. It suggests:
npm install axois
Notice the transposed o and i? That's a real package on npm. So are chalk-cli (vs chalk), lodash-utils (vs lodash), and hundreds more. Attackers register these misspellings hoping you — or your AI assistant — will make the mistake.
In June 2025, researchers at Checkmarx uncovered a campaign targeting packages like colorama (one of Python's most-used terminal color libraries). The attackers published coloramapkgs and colorizator to PyPI, mimicking both Python and JavaScript naming conventions to cast a wider net. The malicious packages delivered platform-specific payloads: on Windows, they disabled Windows Defender and harvested environment variables; on Linux, they opened encrypted persistent backdoors using tools like gs-netcat.
Here's the extra-cruel twist for vibe coders: your AI assistant can be fooled too. A 2025 study from UT San Antonio, Virginia Tech, and the University of Oklahoma tested 16 code-generation models — including GPT-4, Claude, and CodeLlama — generating 576,000 code samples. The models regularly hallucinated package names that don't exist. Attackers now register those hallucinated names pre-emptively — a technique researchers call "slopsquatting." If your AI suggests a package and you install it without verification, you may be installing something an attacker parked there months ago, waiting for exactly this moment.
2. Maintainer Compromise: Hijacking the Keys
This is the scariest category, because the package you trust is the package that attacks you.
The axios incident is the clearest recent example. The attacker didn't touch a single line of axios's actual source code. Instead, they:
- Stole the npm access token of
jasonsaayman, axios's primary maintainer - Changed the account's registered email to an attacker-controlled ProtonMail address, locking out the real owner
- Published two new versions of axios that looked legitimate — same source, same everything — but with one extra line in
package.json:
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^2.1.0",
"plain-crypto-js": "^4.2.1"
}
That last line — plain-crypto-js — was the weapon. It was a typosquat of the legitimate crypto-js library, published from a throwaway account 18 hours before the axios attack to build up a brief history on the registry (evading tools that flag brand-new packages).
plain-crypto-js was never imported anywhere in axios's code. It was a phantom dependency — its only purpose was to run its postinstall script when you ran npm install.
Similar mechanics were at the heart of the September 2025 npm phishing attack, where attackers registered the domain npmjs.help, then sent emails to maintainers impersonating npm support: "Your 2FA credentials need to be updated by September 10." Maintainer Josh Junon later wrote: "Email came from support at npmjs dot help. Looked legitimate at first glance. Not making excuses, just had a long week and a panicky morning." That one phished account led to 200+ compromised packages affecting billions of weekly downloads, including chalk, debug, strip-ansi, and ansi-regex — packages that are transitive dependencies of nearly every JavaScript project in existence.
3. Malicious postinstall Scripts: Code That Runs Before Your Code
This is the technical mechanism that makes all of the above so devastating — and so poorly understood by most developers.
When you run npm install, npm doesn't just download files. It executes code. Any package in your dependency tree can define lifecycle scripts in its package.json:
{
"scripts": {
"preinstall": "node setup.js",
"postinstall": "node setup.js"
}
}
These scripts run automatically, with full access to your filesystem, environment variables, and network — before you've touched a single line of your own code. There's no sandbox. No confirmation dialog. No warning.
In the axios attack, plain-crypto-js@4.2.1's postinstall hook ran node setup.js — a heavily obfuscated dropper that:
- Detected your operating system
- Reached out to the attacker's server at
sfrclak.com:8000 - Downloaded and executed a platform-specific remote access trojan (RAT): a C++ binary on macOS, a PowerShell script on Windows, a Python script on Linux
- Then deleted itself — removing
setup.js, replacing the maliciouspackage.jsonwith a clean decoy stub renamed frompackage.md
After the attack, if you inspected node_modules/plain-crypto-js, you'd see a perfectly clean package with no evidence of a postinstall script ever having existed. npm audit would find nothing. Manual review would find nothing.
This is why the Trail of Bits security team wrote in September 2025: "Dependency scanning only catches known vulnerabilities. It won't catch when a compromised maintainer publishes malware."
What the Attack Actually Looks Like From Your Terminal
Here's the terrifying thing: the axios attack output looked completely normal:
$ npm install axios
added 5 packages, and audited 6 packages in 1s
found 0 vulnerabilities
Zero vulnerabilities. Clean bill of health. Meanwhile, 1.1 seconds into that install, your machine was already calling home to an attacker's server.
This is what distinguishes supply chain attacks from traditional vulnerabilities. There's no CVE. No security advisory. No audit tool alert. The attack is happening during installation, not during execution of vulnerable code.
Five Things You Can Do Right Now
Here's the practical toolkit. None of these require deep security expertise — they're configuration and habit changes that dramatically reduce your attack surface.
1. Use npm ci Instead of npm install in Any Automated Context
# Use this in CI/CD, Docker builds, and scripts:
npm ci
# NOT this:
npm install
npm ci installs exactly what's in your package-lock.json and fails if there's any discrepancy. npm install resolves dependencies fresh every time, which means a new malicious version of a package can silently replace a safe one.
Commit your lockfile. If package-lock.json is in your .gitignore, fix that right now.
2. Disable Lifecycle Scripts in Automated Builds
# Blocks all preinstall/postinstall/install scripts:
npm ci --ignore-scripts
# You can also set this globally:
npm config set ignore-scripts true
This is the single most effective mitigation against the postinstall attack vector. Most packages don't need lifecycle scripts to function — they're used for things like compiling native modules. If a build breaks with --ignore-scripts, investigate why before removing the flag.
3. Set a Minimum Package Release Age
npm config set min-release-age 72h
This setting (introduced in npm 10+) blocks packages published less than 72 hours ago from being installed. The axios attack would have been completely blocked by this setting — the malicious plain-crypto-js@4.2.1 was only a few hours old when the attack ran. The attacker's 18-hour staging gap still wouldn't have been enough.
4. Always Verify Package Names Before Installing
Never trust an AI assistant's package suggestion blindly. Before running npm install <package>, spend 30 seconds checking:
# Check the npm registry page first:
npm info <package-name>
# Look for:
# - Download counts (legitimate popular packages have millions)
# - Publish date (very new = higher risk)
# - Repository link (should match a real GitHub project)
# - Maintainer count and history
For the colorama typosquatting campaign, a quick npm info coloramapkgs would have shown a package with zero weekly downloads and a publish date from that week. That's a red flag.
5. Check for Provenance on Critical Packages
Npm now supports Sigstore-based provenance attestations — cryptographic proof that a package was built from a specific GitHub Actions workflow on a specific commit. The axios attack was detectable because the legitimate axios versions are published via GitHub Actions with OIDC Trusted Publisher, but the malicious versions were published manually with a stolen token.
# Check if a package has provenance:
npm info axios dist.attestations
# A legitimate recent axios publish shows:
# predicateType: 'https://slsa.dev/provenance/v1'
# bundleFormat: 'application/vnd.dev.sigstore.bundle.v0.3+json'
If a new version of a package that normally has provenance shows up without it, treat that as a compromise indicator.
Quick Detection: Was I Hit?
If you think you might have installed a compromised package, here's how to check for the most recent incidents:
# Check for compromised axios versions (March 2026 attack):
npm list axios 2>/dev/null | grep -E "1\.14\.1|0\.30\.4"
# Check for the phantom dependency:
ls node_modules/plain-crypto-js 2>/dev/null && echo "POTENTIALLY COMPROMISED"
# Check for persistence artifacts on Windows (PowerShell):
Get-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run' | Select-Object MicrosoftUpdate
# Check for macOS artifacts:
ls /Library/Caches/com.apple.act.mond 2>/dev/null && echo "CHECK THIS FILE"
If you find signs of compromise: isolate the machine from the network immediately, do not attempt to clean in place, rebuild from a known-good image, and rotate every credential on that machine — npm tokens, SSH keys, AWS/GCP/Azure credentials, .env files, everything.
The Uncomfortable Truth About Vibe Coding
You're using AI to build software faster than ever. That's genuinely great. But AI assistants have a specific weakness in this threat landscape: they will confidently suggest package names, including ones that don't exist — and attackers are now pre-registering those hallucinated names at scale.
In early 2025, Google's AI Overview surfaced a recommendation for @async-mutex/mutex — a malicious typosquat of the legitimate async-mutex library (4 million+ weekly downloads) — as a credible search result. Developers were following that recommendation.
AI makes you fast. But fast + unverified is exactly what supply chain attackers are counting on.
The habit you need: before npm install <anything>, spend 30 seconds on the npm registry page. It's the fastest security check you can do, and it catches the majority of these attacks cold.
Action Checklist
- Add
package-lock.jsonto version control if it isn't already. Remove it from.gitignore. - Replace
npm installwithnpm ciin all CI/CD pipelines, Dockerfiles, and deployment scripts. - Add
--ignore-scriptsto CI installs:npm ci --ignore-scripts - Set minimum release age:
npm config set min-release-age 72h - Before every
npm install <package>, check the registry page: download counts, publish date, maintainer history, linked repository. - Never install a package suggested by an AI without verifying it exists at npmjs.com first.
- Enable 2FA on your npm account — phishing works, but TOTP/hardware keys stop credential replay.
- Check provenance for critical dependencies:
npm info <package> dist.attestations - Set up registry alerts for packages you depend on (Socket.dev and Phylum both offer free monitoring).
- If you think you were hit: isolate, rebuild from clean image, rotate all credentials.
Ask The Guild
Community prompt: Have you ever accidentally installed a typosquatted package, or caught a suspicious dependency in a project? What was your first indicator something was wrong — and what did you do about it? Share your story in the comments. Bonus: what's the weirdest or most suspicious package name you've encountered in the wild?
Tom Hundley is a software architect with 25 years of experience. He has watched the npm registry grow from a curiosity to the single largest software repository in human history — and the single largest attack surface. He writes for developers who build fast and want to stay secure while doing it.