The password manager you trust with your secrets almost became the delivery vector for stealing them.
Earlier this week, security researchers at Checkmarx disclosed an active supply chain attack targeting the Bitwarden CLI via a malicious npm package. It hit close to home for a lot of developers — not just because Bitwarden is widely used, but because it exposes exactly how fragile our toolchain trust really is. This isn't a theoretical attack. It's real, it's ongoing, and if you haven't audited your dependencies yet, stop reading, open a terminal, and do it first.
Done? Good. Now let's talk about what actually happened, why it worked, and what you need to do to not be the next victim.
What Happened
The attack combined two classic supply chain techniques: typosquatting and dependency confusion.
Typosquatting
Attackers published a package called @bitwarden/cli — notice the scoped package under the Bitwarden namespace. The legitimate Bitwarden CLI package is @bitwarden/cli, so at first glance they look identical. But the malicious version was uploaded to npm using a slightly different account, and for a brief window, it appeared in search results alongside (or in place of) the legitimate one.
# Legitimate package
npm install @bitwarden/cli
# Malicious package (visually identical in some search results)
npm install @bitwarden/cli # attacker-controlled account
The delta was in the package metadata, not the name. Without scrutinizing the maintainer account, you'd never know.
Dependency Confusion
The second vector was more insidious. Several internal tooling configurations were identified where bitwarden-cli (note: no scope) was listed as a dependency — presumably an internal or legacy convention. npm's resolution order means a public registry package with that name wins over an internal one if the version number is higher. Attackers published a public bitwarden-cli package with an inflated version (99.9.9), and any CI pipeline pulling dependencies automatically would silently install the attacker's version.
{
"dependencies": {
"bitwarden-cli": "^1.0.0"
}
}
In a confused resolution scenario, npm would see 99.9.9 publicly available and consider it a match for ^1.0.0. Your lockfile would update. Your secrets would be at risk.
The Payload
Checkmarx confirmed the malicious packages contained a post-install hook — the go-to execution vector because it runs automatically when you npm install. The hook reached out to an external command-and-control server, exfiltrating environment variables and any secrets accessible to the install process.
{
"scripts": {
"postinstall": "node ./scripts/setup.js"
}
}
setup.js, needless to say, was not setting anything up for you.
Why This Keeps Working
Every few months we see another supply chain attack, and every time the same discussion plays out: "Why does npm let this happen?" But blaming npm misses the point.
The root issue is implicit trust. When you run npm install, you're executing arbitrary code written by strangers, on your machine, with your credentials in scope. We've normalized this. We shouldn't have.
The Bitwarden attack worked because:
- The namespace was confusable. Scoped packages on npm offer some protection, but they don't prevent account impersonation or namespace-adjacent confusion.
- Postinstall hooks are a loaded gun. They run automatically with no prompt, no review, no opt-in.
- Lockfiles were stale or absent. A committed, integrity-checked lockfile would have caught the version substitution immediately.
- No one was watching the registry. Tools like Socket.dev and Snyk's reachability analysis exist precisely to flag this. Most teams aren't using them.
What Bitwarden Did
Bitwarden moved quickly. Within hours of disclosure:
- The malicious packages were reported to npm and removed
- Bitwarden published a security advisory confirming the scope of the attack
- They rotated their own publishing keys and audited maintainer access
- A Canary token was deployed to detect any further exploitation attempts
Credit where it's due — this was a good incident response. But "good incident response" means the fire got contained after it started. The prevention side is what needs work, and that's on all of us, not just Bitwarden.
What You Need to Do Right Now
Here's the actionable checklist. Copy this into your incident response runbook.
1. Audit Your Bitwarden CLI Usage
# Check if you have any bitwarden-related packages installed
npm ls | grep -i bitwarden
npx audit-ci --moderate
# Check your global installs too
npm list -g | grep -i bitwarden
If you find anything, verify the package version and maintainer against Bitwarden's official security advisory before doing anything with it.
2. Pin Your Dependency Versions
Stop using ^ and ~ range specifiers for anything that touches production or CI environments. Pin exact versions.
{
"dependencies": {
"@bitwarden/cli": "2025.1.0"
}
}
Ranges are convenient for local dev. They're a liability in production.
3. Commit and Enforce Lockfiles
Your package-lock.json or yarn.lock is your source of truth. Treat it like code.
# npm — verify integrity against lockfile
npm ci # NOT npm install
# Enforce in CI: fail if lockfile would change
npm ci --audit
npm ci exits non-zero if the lockfile doesn't match package.json. Use it in every CI pipeline. Not npm install. npm ci.
4. Enable npm Audit in CI
# GitHub Actions example
- name: Install dependencies
run: npm ci
- name: Run security audit
run: npm audit --audit-level=moderate
This catches known vulnerabilities. It won't catch brand-new malicious packages, but it's table stakes.
5. Add Socket.dev or Snyk to Your Pipeline
These tools go beyond npm audit by analyzing supply chain risk in real time — flagging packages with new maintainers, unusual install scripts, or suspicious network calls.
# Socket.dev CLI
npx socket check @bitwarden/cli
# Snyk
snyk test --all-projects
Socket.dev specifically detects postinstall scripts that make outbound network calls — exactly what this attack used. If you're not running something like this, you're flying blind.
6. Audit Your Internal Package Names
If your organization uses private packages, make sure every internal package name either:
- Uses a private registry (Verdaccio, Artifactory, GitHub Packages) with explicit scope resolution
- Is scoped under your own npm org namespace
Update your .npmrc to enforce registry routing:
@yourcompany:registry=https://your-private-registry.example.com
And add a publishConfig to every internal package to prevent accidental public publishing:
{
"publishConfig": {
"registry": "https://your-private-registry.example.com"
}
}
This won't fully prevent dependency confusion, but it drastically reduces the attack surface.
7. Restrict Postinstall Scripts in Production
npm lets you disable lifecycle scripts:
npm install --ignore-scripts
Yes, some legitimate packages need postinstall hooks. But in a CI/CD context, you can often disable them safely — especially for packages you've already vetted locally. Run your normal install locally, commit the lockfile, and use --ignore-scripts in your pipeline.
The Bigger Picture
We're in the golden age of supply chain attacks and the ecosystem is not keeping up. The npm registry processes millions of packages and has essentially no verification gatekeeping. Anyone can publish. Anyone can typosquat. The only real defense is layered vigilance on your end.
Some uncomfortable truths:
Lockfiles are not optional. If your repo has no committed lockfile, you're one npm install on a fresh machine away from installing whatever attackers have promoted to the top of version resolution.
CI should never have broad secret access. The Bitwarden attack targeted environment variables. If your CI runner has AWS_SECRET_ACCESS_KEY, DATABASE_URL, and your API tokens all in scope during the install phase, an attacker who compromises one dependency gets all of it. Scope your secrets. Inject them only where needed.
"It's open source, someone would have noticed" is dead. XZ Utils happened. This happened. The assumption that the community will catch everything is naïve at scale.
The Incident Response Runbook (Paste-Ready)
## Supply Chain Attack Response Checklist
### Immediate (first 30 minutes)
- [ ] Identify affected packages from security advisory
- [ ] Run `npm ls | grep <package>` across all repos
- [ ] Freeze deploys until audit is complete
- [ ] Rotate any secrets accessible during install phase (env vars, tokens, keys)
### Short-term (first 24 hours)
- [ ] Audit CI logs for abnormal network calls during install steps
- [ ] Pin exact versions for affected and adjacent packages
- [ ] Run `npm ci` to regenerate lockfile from pinned versions
- [ ] Enable npm audit in all pipelines if not already present
### Medium-term (first week)
- [ ] Add Socket.dev or Snyk to CI pipelines
- [ ] Review all packages with postinstall scripts: `cat node_modules/*/package.json | jq 'select(.scripts.postinstall) | .name'`
- [ ] Audit internal package names for dependency confusion risk
- [ ] Configure .npmrc to scope internal packages to private registry
### Ongoing
- [ ] Subscribe to security advisories for your top 20 dependencies
- [ ] Use `npm ci` (not `npm install`) in all CI environments
- [ ] Review and rotate CI secrets on a quarterly schedule
The Bitwarden CLI attack is a wake-up call, not an anomaly. Supply chain attacks are the dominant threat vector in developer tooling right now, and the npm ecosystem is a particularly juicy target precisely because developers trust it implicitly.
Lock your dependencies. Audit your CI. Use tooling that watches the registry for you. The cost of implementing these controls is a few hours. The cost of not implementing them is your infrastructure, your secrets, and your users' data.
That's not a tradeoff worth making.
Top comments (0)