Back to all incidents

Bitwarden CLI — npm supply-chain compromise (downstream of Checkmarx)

Trojanised @bitwarden/cli 2026.4.0 lived on npm for 93 minutes; root cause was a compromised Checkmarx GitHub Action that altered Bitwarden's publish step without touching its source.

Target
Bitwarden CLI — npm supply-chain compromise (downstream of Checkmarx)
Date public
22 April 2026
Sector
Technology
Attack type
Supply Chain
Threat actor
TeamPCP (Shai-Hulud campaign cluster)
Severity
High
Region
Global

Bitwarden's command-line tool is a small program developers and IT teams use to script the password manager from a terminal. On 22 April 2026 a poisoned version was published to npm — the registry every JavaScript build pulls from — for ninety-three minutes before being pulled. About 334 people installed it in that window. Bitwarden's source code was not modified. Attackers had compromised a GitHub Action from Checkmarx that Bitwarden's build pipeline depended on, and used it to alter only the published package. The trojanised CLI harvested developer cloud and CI credentials and tried to re-infect any other npm packages the victim could publish. End-user password vaults were never reached. The interesting question for defenders is not the CLI itself but the trust they extend to every third-party Action sitting in their build pipelines.

What happened

On 22 April 2026, between 5:57pm and 7:30pm Eastern Time, version 2026.4.0 of the npm package @bitwarden/cli sat on the public npm registry as the apparent latest release of Bitwarden’s command-line client. The version was a trojan. Roughly 334 installations completed in the ninety-three-minute window before Bitwarden pulled the package, revoked the publishing credential, and re-released a clean version 2026.4.1 that was functionally a re-publication of the unaffected 2026.3.0.

The Bitwarden CLI source repository was never modified. The attack lived entirely in the publishing step — the post-build phase that takes the compiled artefact, wraps it as an npm package, and pushes it to the registry. Bitwarden’s response statement, the joint analysis from Endor Labs, Socket, Palo Alto Networks Unit 42 and Trend Micro, and the SANS Internet Storm Center campaign tracker all agree on the chain. The entry point was a compromised version of the third-party GitHub Action checkmarx/ast-github-action, which Bitwarden’s CI/CD workflow used to scan its code on every build. End-user vault data was not accessed and there is no public evidence to suggest the Bitwarden production infrastructure itself was reached.

The interesting story is not the CLI. It is what the attacker was holding when they decided to use it.

How it worked

The chain starts upstream of Bitwarden, at Checkmarx, and upstream of Checkmarx, at Aqua Security’s Trivy. In late 2025 the same actor cluster — tracked publicly as TeamPCP and associated with the “Shai-Hulud” malware lineage — compromised the Trivy supply chain and used it to harvest credentials from downstream Trivy users. Among the credentials harvested were tokens belonging to Checkmarx’s aqua-bot service account. On 28 February 2026 Checkmarx detected the intrusion, rotated credentials, and stood the campaign down on its own infrastructure. By 23 March, TeamPCP was back inside Checkmarx’s GitHub organisation. The root cause of the second intrusion, per Checkmarx’s own writeup, was an incomplete credential rotation: either the original token was not fully revoked or, more likely, the attacker observed the rotation while still in the environment and harvested the replacement before losing access.

Once back inside, TeamPCP backdoored two Checkmarx-published GitHub Actions — checkmarx/kics-github-action and checkmarx/ast-github-action — by pushing tagged releases that pointed at malicious commits. Both are used by thousands of downstream repositories as part of routine secure-development scanning, including Bitwarden’s. Because most repositories pin Actions by tag rather than by commit SHA, the malicious tag silently propagated into every CI pipeline that referenced the Action by its floating tag on its next workflow run.

When the malicious ast-github-action ran inside Bitwarden’s CI job, it did two things. It harvested the runner’s secrets — the AWS, GitHub and npm tokens Bitwarden’s publish workflow needs to operate — and it submitted those credentials to attacker infrastructure. The attacker then used the npm token to publish @bitwarden/cli@2026.4.0 directly. Bitwarden’s source repository was never touched.

The trojan package itself was the more sophisticated half. The modified package.json introduced a preinstall script — a Node bootstrapper called bwsetup.js that ran automatically on npm install, downloaded the Bun JavaScript runtime if absent, and then executed an obfuscated Bun bundle named bw1.js. The same bw_setup.js loader was wired in as the package’s primary bw entry point, so the malicious code ran both at install time and at every subsequent CLI invocation. Bun was chosen because most enterprise security tooling does not yet inspect Bun bundles the way it inspects Node.

The bw1.js payload combined six distinct capabilities. It harvested cloud and CI credentials from Azure, AWS, GCP, GitHub and npm secret surfaces, plus SSH material, shell history and configuration files belonging to AI coding assistants — Cursor, Codex, Claude Code, MCP servers — which had only recently started showing up as a named target class in supply-chain malware. It exfiltrated the harvested data, encrypted with AES-256-GCM, to a typo-squatted domain audit.checkmarx[.]cx. It implemented a self-propagating npm worm: if the victim’s stolen npm token had publish rights against other packages, the payload re-published those packages with the same malicious preinstall hook. It established a dead-drop command-and-control channel using GitHub commits in attacker-controlled repositories with RSA-signed command delivery, allowing the attacker to issue further instructions even if specific exfil endpoints were taken down. It installed shell-RC persistence to survive reboots. And, most operationally interesting, it deployed a small daemon called gh-token-monitor that polled GitHub every sixty seconds and, on detecting that the stolen token had been revoked, executed rm -rf ~/ against the victim’s home directory. The defender lesson there is unambiguous: rotate the credential after you have removed the persistence, not before.

The reused obfuscation routine in bw1.js__decodeScrambled with the seed 0x3039 — is the same one observed in the KICS backdoor, which is the strongest single piece of evidence tying the Bitwarden compromise to the broader TeamPCP campaign rather than to an opportunistic separate actor.

Timeline

  • Late 2025 — TeamPCP compromises the Trivy supply chain at Aqua Security; harvests credentials downstream, including a Checkmarx aqua-bot service-account token.
  • 28 February 2026 — Checkmarx detects intrusion, rotates credentials, attempts to evict the actor.
  • 23 March 2026 — TeamPCP re-enters Checkmarx’s GitHub organisation following incomplete credential rotation; tags malicious releases of checkmarx/kics-github-action and checkmarx/ast-github-action.
  • 22 April 2026, 5:57pm ET — Trojanised @bitwarden/cli@2026.4.0 published to npm by attacker using credentials harvested from a Bitwarden CI run.
  • 22 April 2026, 7:30pm ET — Bitwarden pulls the package after detection by security researchers and community reporting; revokes npm credentials.
  • 23 April 2026 — Bitwarden publishes community statement; releases 2026.4.1 (a clean republication of 2026.3.0).
  • 24–27 April 2026 — Endor Labs, Socket, Palo Alto Networks Unit 42, Trend Micro and SANS ISC publish technical analyses.
  • 11 May 2026 — TeamPCP resurfaces with a separate compromise of the Checkmarx Jenkins AST plugin, indicating the cluster retained or regained access despite the April containment claim.

What defenders should learn

The Bitwarden CLI compromise is a clean case study in third-party CI/CD trust. The package was malicious for ninety-three minutes. The Bitwarden source repository was clean throughout. End-user vault data was never reached. The harm was done at the publishing edge, by a third-party Action that Bitwarden’s build pipeline trusted to do its job. That is the operational lesson — and it is everyone else’s lesson too, because almost every modern build pipeline references between five and fifty third-party GitHub Actions, none of which are typically pinned to immutable commit SHAs, none of which are typically inventoried as part of the corporate vendor risk register, and almost all of which run with whatever secrets the build needs to do its work.

The high-leverage defender actions, in order of effort: pin every third-party GitHub Action to a commit SHA rather than a tag, so that a backdoored release does not silently propagate; allowlist the third-party Actions your organisation is permitted to use, and review that list at the cadence you review SaaS vendors; treat your CI runner’s outbound network as in-scope for egress monitoring, since that is where exfiltration lives; and confirm that the credentials your build pipeline holds are short-lived, scope-restricted, and minted via OIDC federation rather than long-lived PATs that sit in the GitHub secrets store forever. None of those are novel ideas. The novel observation is that the threat actors are now organised enough to chain three independent supply-chain compromises across two ecosystems and a runtime in under sixty days, and that the post-disclosure window between announcement and the next public re-fire is shrinking.

Andy will write more on the segmentation implications when time allows. The immediate point — that the trust boundary of a CI pipeline is the boundary of every dependency it pulls — is independent of the framing.

Sources

Back to all incidents