
The Bitwarden CLI Supply Chain Attack: What Happened and What to Do | Blog | Endor Labs
Analysis of a supply-chain compromise of @bitwarden/cli version 2026.4.0, published to npm for ~1.5 hours on April 22, 2026, which added a preinstall script (bwsetup.js) that downloads a Bun runtime and executes a 10MB obfuscated payload (bw1.js) harvesting CI secrets like SSH keys and API tokens and exfiltrating them to attacker-created public GitHub repos in victims' namespaces. The compromise is linked to a prior breach of Checkmarx's GitHub Actions (checkmarx/ast-github-action), used in Bitwarden's repo, and the payload includes CI/CD platform detection across 30+ platforms and a Russian-locale self-exclusion check consistent with Eastern European threat actor tooling, framed as part of the "Shai-Hulud" campaign lineage.
What it is
A supply-chain compromise of the npm package @bitwarden/cli, malicious version 2026.4.0, live on npm ~1.5 hours (5:57–7:30 PM ET, April 22, 2026). Bitwarden attributes it to the recent breach of Checkmarx's GitHub Actions, specifically checkmarx/ast-github-action, which is used in Bitwarden's own repo. The summary covers the campaign framing; what follows are the concrete artifacts and remediation the page adds.
How it works
The malicious version adds two files absent from 2026.3.0: bwsetup.js and bw1.js. package.json is modified to add a preinstall script (node bwsetup.js) and to remap the bw bin entry from build/bw.js to bwsetup.js — so both install-time and every later bw invocation route through the bootstrap. bwsetup.js is plain Node ESM: it checks for an existing Bun (execFileSync("bun", ["--version"])), and if absent downloads Bun 1.3.13 from the official GitHub release ZIP, extracts the binary under the package directory, and runs bw1.js with it. bw1.js is a single-line, ~10 MB (10,154,904 bytes) javascript-obfuscator bundle: array-rotation IIFE, a 43,436-entry string table (_0x1ee1), and a second scrambled-alphabet cipher (seed 0x3039) hiding the C2 domain, shell commands, and credential paths.
Anti-analysis gates run before any malicious activity:
mz0()— exits cleanly if locale orLC_*/LANG/LANGUAGEstarts withru.tz0()— a switch over 30+ CI/CD fingerprints (GITHUB_ACTIONS,GITLAB_CI,CIRCLECI,JENKINS_URL,TRAVIS,BUILDKITE, cloud build IDs) treated as "allowed to run."- Single-instance guard
FE— a lock file attmpdir() + "tmp.987654321.lock", checked viaprocess.kill(pid, 0).
Preconditions / Affected
@bitwarden/cli@2026.4.0installed or resolved during the ~1.5-hour window on April 22, 2026.- CI/CD environments are the intended target — the payload is built to run in CI and self-excludes elsewhere.
- Note: an existing Bun install can cause
bwsetup.jsto return early, thoughbw1.jsstill ships beside it.
What to do
- Last known-clean release:
@bitwarden/cli@2026.3.0. Fixed re-release:@bitwarden/cli@2026.4.1(Apr 23, 2026, 4:45 PM GMT+2). Pin to one of these. - Hunt for the artifacts: files
bwsetup.js/bw1.js, apreinstallrunningnode bwsetup.js, abinmappingbwtobwsetup.js, and the lock filetmp.987654321.lockin$TMPDIR. - If exposed, rotate CI secrets (SSH keys, API tokens) and check victims' GitHub namespaces for attacker-created public repos holding exfiltrated data.
Caveats
The page is explicitly a developing story. It also flags an internal inconsistency: the blog claims non-CI plus failed daemonization leads to exit, but tz0() reads as an allowlist of CI environments — the exact run/no-run logic outside CI is unresolved. The credential-harvesting section ("Seven independent collectors") is cut off in the fetched text.
Organisations named here
- checkmarx/ast-github-action
- Shai-Hulud
Technologies named here
- npm
- GitHub Actions
- Bun
- Node.js
- Bitwarden
- Bitwarden CLI
- Checkmarx
- javascript-obfuscator
Related in Supply Chain


