How a compromised security scanner let attackers publish a credential-stealing worm under Bitwarden's own name
The package that landed in developers' projects on April 22, 2026 looked exactly like the official Bitwarden command-line tool. It came from the official @bitwarden scope on npm. It was published by Bitwarden's own automated build pipeline. It passed every trust signal a developer or security tool would check. And it was stealing SSH keys, cloud credentials, and AI assistant tokens within seconds of installation, before a single line of Bitwarden's own code had been touched.
The attacker never broke into Bitwarden directly. They broke into a third-party security scanning tool that Bitwarden's build pipeline trusted, and used that foothold to silently rewrap the legitimate Bitwarden CLI with a credential-stealing worm before it was published. The result was a package that was simultaneously genuine and malicious: real Bitwarden code, real Bitwarden signature, real Bitwarden distribution channel, with a hidden layer added by an attacker who never needed to touch Bitwarden's own systems.
The most alarming detail is not the credential theft. It is what happened next. Once the worm found npm tokens on a developer's machine, it automatically unpacked every npm package that developer had publish rights to, injected itself into each one, and republished them to their downstream consumers, without any further action from the attacker. A single developer who ran npm install @bitwarden/cli during the 93-minute exposure window could have become an unwitting distribution node for the same worm, pushing it to every project that depended on their packages.
Narrative · 6 min read
The Context
Bitwarden is one of the most widely used open-source password managers in the world, serving over 10 million users and 50,000 business customers. Its command-line interface (CLI), distributed as @bitwarden/cli on the npm registry, is used by developers and IT teams to automate credential management in scripts and build pipelines. The package had over 78,000 weekly downloads at the time of the attack.
Checkmarx is a cybersecurity company that makes application security testing tools. Many software teams, including Bitwarden, use Checkmarx's GitHub Actions in their build pipelines to automatically scan code for vulnerabilities.
The Attack, Phase by Phase
Phase 1: Upstream Breach of Checkmarx Tooling
The attack began not at Bitwarden but at Checkmarx. In March 2026, a threat actor tracked as TeamPCP compromised Checkmarx's GitHub Actions infrastructure, injecting credential-stealing code into two widely used actions: checkmarx/ast-github-action and checkmarx/kics-github-action. They also trojanized Checkmarx's KICS Docker images. Checkmarx disclosed this publicly on March 23, 2026.
By compromising the scanner, TeamPCP gained a foothold inside the build pipelines of every organization using these tools—including Bitwarden—without ever attacking those organizations directly.
Phase 2: Pipeline Injection and Malicious Package Publication
Bitwarden's automated release pipeline, defined in publish-ci.yml in the bitwarden/clients repository, referenced the compromised checkmarx/ast-github-action. On April 22, 2026, attackers used harvested credentials to abuse this pipeline step.
They did not rewrite Bitwarden's code. Instead, they added a hidden layer on top of the legitimate CLI 2026.3.0 codebase: a bootstrapper script called bw_setup.js and a 9.7-megabyte obfuscated payload called bw1.js. The result was published to npm as @bitwarden/cli@2026.4.0 under Bitwarden's own trusted scope. A forensic clue confirmed the tampering: the outer version said 2026.4.0, but the version embedded inside the application code still said 2026.3.0, because the malicious layer was added after the build.
The package was live at 5:57 PM ET. Bitwarden contained the incident at 7:30 PM ET. The exposure window was 93 minutes.
Phase 3: Credential Harvesting and Multi-Channel Exfiltration
When a developer ran npm install @bitwarden/cli, the malicious package.json triggered a preinstall hook that silently downloaded the Bun JavaScript runtime from GitHub's official servers and used it to execute bw1.js. The malware ran both on install and on every subsequent use of the bw command.
The payload swept SSH private keys, AWS and Google Cloud credentials, npm authentication tokens, Claude and Kiro AI assistant configuration files, Git credentials, .env files, and shell history. It also called cloud secret manager APIs directly using whatever credentials it found, pulling secrets from AWS, Azure, and Google Cloud vaults.
Stolen data was encrypted and sent to a domain impersonating Checkmarx: audit.checkmarx.cx. If that channel failed, the malware created a public GitHub repository under the victim's own account using stolen GitHub tokens, committed the encrypted data there with messages like LongLiveTheResistanceAgainstMachines, and named repositories using Dune-universe vocabulary such as fremen-sandworm-441. OX Security confirmed real user data was exfiltrated before containment.
Phase 4: Worm Propagation Across the npm Ecosystem
Using stolen npm tokens, the malware enumerated every npm package the victim had rights to publish. For each one, it downloaded the current version, injected a new preinstall dropper, bumped the patch version, and republished using Bun's native publish capability. No further attacker involvement was required. Each infected developer became a new distribution node.
Simultaneously, stolen GitHub tokens injected a malicious workflow named "Formatter" into every repository the token could write to. The workflow extracted the entire CI/CD secrets context, saved it as a downloadable artifact, then deleted the branch and workflow run to erase evidence.
What Made This Possible
-
Mutable third-party actions in the build pipeline. Bitwarden's
publish-ci.ymlreferencedcheckmarx/ast-github-actionby a tag rather than a fixed commit hash. Tags are mutable: the repository owner can change what a tag points to at any time. When Checkmarx was compromised, the tag pointed to attacker-controlled code, and Bitwarden's pipeline ran it with full publishing credentials. -
Elevated credentials available to pipeline steps. The build pipeline had authority to publish under the
@bitwardenscope. When an attacker controlled a step in that pipeline, they inherited that authority with no distinction between a legitimate build step and a compromised one. -
No independent verification of the published artifact. The package that reached npm was never compared against what Bitwarden's engineers had actually built. No checksum comparison, no reproducible build verification, no out-of-band confirmation. The pipeline's output was trusted by definition.
What Should Have Stopped This
- Pin third-party actions to a specific commit hash. A pinned hash cannot be silently changed. If
checkmarx/ast-github-actionhad been pinned, the compromised tag would have had no effect on Bitwarden's pipeline. - Separate publishing credentials from build credentials. The npm token that could publish under
@bitwardenshould not have been available to every pipeline step. A dedicated, isolated publishing step with a scoped token limits what an attacker can do even controlling an earlier step. - Reproducible builds with independent verification. Comparing the pipeline's artifact against an independently built reference would have caught the version mismatch (2026.4.0 outer, 2026.3.0 inner) automatically.
- Audit preinstall hooks before installation. Tools that inspect
package.jsonfor preinstall scripts before runningnpm installwould have flagged the unexpectedbw_setup.jshook in a package that had never previously used one.
The Takeaway
This attack follows the same pattern as the Stryker Intune wipe and the Axios build-time injection: a trusted administrative tool was weaponized against the organization it was built to protect. In the Stryker case, it was a device management platform. In the Axios case, a build-time dependency. Here, a security scanner embedded in the build pipeline. The meta-pattern: systems fail when they grant elevated authority to a boundary the attacker controls.
What makes this incident distinct is propagation. Previous supply chain attacks required the attacker to maintain active infrastructure to spread. This one built a self-replicating distribution network out of the victims themselves, using their own publishing credentials, their own trusted package names, and their own downstream relationships. The attacker's involvement ended the moment the first developer installed the package.
Pattern to remember: A build pipeline's security posture is determined by the least-trusted tool it invokes with elevated credentials, not by the security of the vendor's own code.
What changed: Supply chain attacks can now self-propagate through the npm ecosystem using victims' own publishing identities, requiring no ongoing attacker infrastructure after the initial infection.
Technical Deep Dive · 4 min
The Technical Mechanism
The attack chain begins with a tag-based reference to a compromised GitHub Action. Bitwarden's publish-ci.yml workflow in github.com/bitwarden/clients invoked checkmarx/ast-github-action using a mutable tag. After TeamPCP compromised Checkmarx's infrastructure in March 2026, this tag resolved to attacker-controlled code with access to the workflow's environment, including the NPM_ACCESS_TOKEN secret scoped to publish under @bitwarden.
The injected packaging step wrapped the legitimate Bitwarden CLI 2026.3.0 build artifacts with two new files. bw_setup.js (132 lines, plaintext) served as the bootstrapper: it detected the victim's OS and architecture, downloaded Bun runtime 1.3.13 from github.com/oven-sh/bun/releases if not already present, and executed bw1.js. Using Bun rather than Node.js was a deliberate evasion choice: it moved execution off the expected Node.js code path and gave the attacker access to Bun-specific APIs for decompressing embedded payloads, while keeping bw_setup.js itself clean enough to pass casual review.
bw1.js is a 9.7 MB Bun bundle obfuscated via obfuscator.io with a 43,436-entry string lookup table and a seeded pseudo-random scramble table protecting the most sensitive strings, including the C2 domain. The payload executed six capability blobs:
-
Local credential harvester: Targeted
~/.ssh/id*,~/.aws/credentials,~/.config/gcloud/credentials.db,~/.npmrc,~/.claude.json,~/.claude/mcp.json,~/.kiro/settings/mcp.json,.git/config,.git-credentials,.env, and shell histories. Also extracted the GitHub CLI's cached authentication token viagh auth token, catching tokens stored in system keychains rather than on disk. -
Cloud secret manager collector: Called AWS SSM Parameter Store, AWS Secrets Manager, Azure Key Vault, and GCP Secret Manager APIs directly using ambient credentials found in step 1.
-
Exfiltration packager: Encrypted all stolen data with AES-256-GCM using a 32-byte random session key, then RSA-2048 OAEP encrypted the session key with the attacker's public key, bundled as
tpcp.tar.gz. The same RSA key and archive format appear in prior TeamPCP operations against LiteLLM and telnyx. -
Primary C2 channel: HTTP POST to
audit.checkmarx.cx/v1/telemetry(resolving to94.154.172.43), a domain impersonating the legitimate Checkmarx brand. -
GitHub dead-drop fallback: If the primary channel failed, the malware used stolen GitHub Personal Access Tokens (PATs) to create a new public repository under the victim's own account, commit
tpcp.tar.gzwith messages following the patternLongLiveTheResistanceAgainstMachines, and name the repository using randomized Dune-universe vocabulary (fremen-sandworm-441,harkonnen-melange-7). Repository description:Shai-Hulud: The Third Coming. -
npm worm and GitHub Actions injector: Enumerated all packages the victim's npm token could publish to, unpacked each tarball, replaced the preinstall script with a new dropper (
node setup.mjs), bumped the patch version, and republished via Bun's native publish API. Simultaneously injected a GitHub Actions workflow namedFormatterinto every repository the stolen GitHub token could write to, serializing the entiresecretscontext into a downloadable artifact, then deleting the branch and workflow run.
The malware included a Russian-language execution check: it did not run if the host machine's locale was configured to Russian.
The version mismatch between the outer package.json (2026.4.0) and the embedded build/bw.js metadata (2026.3.0) is the primary forensic indicator confirming the malicious layer was added post-build rather than through the normal vendor pipeline.
CVE and Advisories
A CVE for @bitwarden/cli@2026.4.0 was announced on April 23, 2026. The CVE ID had not been assigned at time of publication; this post will be updated when the canonical record is available at cve.org.
The upstream Checkmarx compromise was disclosed in the Checkmarx Security Update (March 23, 2026), which confirmed that checkmarx/ast-github-action and checkmarx/kics-github-action were affected, and identified checkmarx.zone as the attacker-controlled C2 domain used in the March wave.
MITRE ATT&CK Mapping
| Technique ID | ATT&CK name | How it appeared |
|---|---|---|
| T1195.001 | Supply Chain Compromise: Compromise Software Dependencies and Development Tools | TeamPCP compromised Checkmarx's GitHub Actions to gain access to downstream build pipelines, including Bitwarden's. |
| T1059.007 | Command and Scripting Interpreter: JavaScript | The payload bw1.js is a heavily obfuscated JavaScript bundle executed via the Bun runtime. |
| T1552.001 | Unsecured Credentials: Credentials in Files | The harvester swept a hardcoded list of credential files including SSH keys, cloud configs, npm tokens, and shell histories. |
| T1552.004 | Unsecured Credentials: Private Keys | SSH private keys were explicitly targeted by the credential harvester. |
| T1555 | Credentials from Password Stores | The malware called AWS SSM Parameter Store, AWS Secrets Manager, Azure Key Vault, and GCP Secret Manager APIs directly. |
| T1567.001 | Exfiltration Over Web Service: Exfiltration to Code Repository | The fallback C2 channel committed encrypted payloads to public GitHub repositories created under the victim's own account. |
| T1053 | Scheduled Task/Job | The preinstall hook and binary entrypoint rewiring ensured the payload fired at both install time and every subsequent invocation. |
| T1496 | Resource Hijacking | Stolen npm tokens were used to republish trojanized versions of victim-owned packages to downstream consumers. |
Indicators of Compromise
Network Indicators
audit.checkmarx.cxresolving to94.154.172.43(primary C2, impersonating Checkmarx)checkmarx.zone(C2 domain used in the March 2026 Checkmarx wave, per Checkmarx's advisory)- Outbound connections to
github.com/oven-sh/bun/releasesfrom CI runners or developer workstations (Bun runtime download)
File System Indicators
- Presence of
bw_setup.jsorbw1.jsin the npm package cache or projectnode_modules/@bitwarden/cli/ tpcp.tar.gzin temporary directories- Unexpected public GitHub repositories named with Dune-universe vocabulary (
fremen-*,harkonnen-*,sandworm-*) under victim accounts
npm and GitHub Indicators
@bitwarden/cliversion2026.4.0inpackage.jsonorpackage-lock.json(deprecated; upgrade to2026.4.1)- GitHub Actions workflow named
Formatterappearing in repositories without a corresponding commit in the default branch history - Commit messages matching the pattern
LongLiveTheResistanceAgainstMachinesin any repository
Detection Difficulty
The worm's cleanup behavior (deleting the injected branch and workflow run after extracting secrets) means GitHub audit logs are the primary detection surface for the Actions injection component. Organizations should query their GitHub audit log for workflow creation and deletion events on April 22, 2026 and the days following. The Russian-language execution check means the malware produced no artifacts on Russian-locale machines, which may create gaps in forensic timelines for organizations with globally distributed developer populations.
Attribution
Multiple security firms attribute this attack to TeamPCP (also tracked as @pcpcats), a cloud-native criminal operation active since at least September 2025. Palo Alto Unit 42, StepSecurity, Socket, and GitGuardian all name TeamPCP as the responsible actor. Prior confirmed victims include Aqua Security's Trivy scanner, LiteLLM, and telnyx, across npm, PyPI, Docker Hub, GitHub Actions, and VS Code extensions.
Socket researchers note an attribution wrinkle: the Bitwarden payload carries ideological branding not seen in earlier TeamPCP operations, including the Shai-Hulud: The Third Coming repository marker, Butlerian Jihad debug strings, and commit messages proclaiming resistance against machines. Socket assesses this could indicate a splinter group, a different operator sharing infrastructure, or a deliberate shift in campaign posture. OX Security notes the Russian-language execution check as a potential indicator of origin, though this technique is common and not definitive. No nation-state link has been established by any firm.
The Shai-Hulud worm lineage dates to September 2025 (original campaign), November 2025 (Shai-Hulud 2.0, tracked by Datadog, Microsoft, and GitLab), and February 2026 (SANDWORM_MODE variant, tracked by Socket). This April 2026 incident is the third major wave.
Primary Sources
- 01.TeamPCP Campaign Spreads to npm via a Hijacked Bitwarden CLI
JFrog Security Research · April 23, 2026
- 02.Shai-Hulud: The Third Coming. Bitwarden CLI Backdoored in Latest Supply Chain Campaign
OX Security · April 23, 2026
- 03.Compromised Bitwarden CLI Poisons AI Assistants and Spreads as npm Worm
Mend.io · April 23, 2026
- 04.Is Shai-Hulud Back? Compromised Bitwarden CLI Contains a Self-Propagating npm Worm
Aikido Security · April 23, 2026
- 05.Bitwarden CLI Compromised in Ongoing Checkmarx Supply Chain Campaign
The Hacker News · April 23, 2026
- 06.Checkmarx Security Update
Checkmarx · March 23, 2026
- 07.Bitwarden CLI npm package compromised to steal developer credentials
BleepingComputer · April 23, 2026
- 08.TeamPCP Hijacks Bitwarden CLI, Uses Dependabot to Deploy Shai-Hulud Malware
Hackread / GitGuardian · April 24, 2026