Bitwarden CLI was malicious on npm for 93 minutes

Between 5:57 and 7:30 PM ET on April 22, 2026, the npm tarball for @bitwarden/[email protected] was a credential harvester. Bitwarden contained it inside two hours. Any fresh install during that window should be treated as exposed.

TL;DR

On April 22, 2026, between 17:57 and 19:30 US Eastern Time, the npm tarball for @bitwarden/[email protected] was a malicious credential harvester. The Bitwarden security team identified the compromise, pulled the malicious build, and republished a clean version inside roughly 93 minutes. The package is part of the broader Checkmarx supply-chain campaign that has been hitting developer-facing tools over the last several weeks.

If you ran npm install -g @bitwarden/[email protected] (or anything that resolved to that version, like an @bitwarden/cli@latest against a fresh lockfile) during that 93-minute window, the install ran a credential harvester. It targeted GitHub and npm tokens, SSH keys, .env files, shell history, GitHub Actions secrets, and any cloud-provider credentials it found. Rotate everything and audit downstream.

This one is different from the pgserve cross-registry worm and the Asurion impersonation campaign. The package is a password manager CLI. Many of us run it on the machines that hold the best secrets. The malicious window was also short: 93 minutes. Short is good for the ecosystem; it is not comforting if your CI ran during those 93 minutes.

What happened

Bitwarden’s public incident response and The Hacker News line up on the timeline. Approximate sequence (US Eastern Time, since that is how Bitwarden has reported it):

  • April 22, 17:57 ET: malicious @bitwarden/[email protected] tarball appears on npm. The version number matches the release pattern Bitwarden uses (YEAR.MONTH.PATCH). The published artifact is signed and the metadata looks correct.
  • April 22, ~18:30 ET: automated scanners (Socket, Snyk) begin flagging the artifact. Internal Bitwarden monitoring also fires.
  • April 22, ~19:00 ET: Bitwarden security team confirms the compromise. They start the takedown process with npm.
  • April 22, 19:30 ET: malicious version pulled from npm. The previous known-good version is restored on the latest dist-tag. Window closes after 93 minutes.
  • April 22, late evening: Bitwarden publishes a public statement acknowledging the incident, naming the malicious version, and providing guidance for affected users.
  • April 23: third-party analyses appear from Socket, Snyk, and others. Indicators of compromise are published.

The compromise vector is described by Bitwarden as “an issue within the npm delivery path”. They are not (publicly, yet) attributing it to a maintainer-account theft. Reading between the lines and comparing to the recent Checkmarx-campaign reports, the most consistent picture is that the campaign got into something adjacent to the publish process: a CI artifact, a build tool, an intermediate step that produces the npm tarball before it is signed and uploaded. Bitwarden has trusted publishing set up; the published artifact had valid provenance. The problem was that the artifact itself, before signing, had been tampered with.

That distinction matters. The signature was correct. The provenance was correct. The trusted-publishing pipeline did what it was supposed to do. What it produced and signed was malicious, because the input to the pipeline was malicious.

The harvest scope

Per The Hacker News, the malicious install script targeted:

  • GitHub personal access tokens and OAuth tokens (~/.gitconfig, ~/.config/gh/config.yml, environment variables)
  • npm tokens (~/.npmrc, including any _authToken lines for any registry)
  • SSH keys (~/.ssh/id_* files)
  • .env files in the current working directory and parent directories
  • Shell history (~/.bash_history, ~/.zsh_history)
  • GitHub Actions secrets, when running in a CI context (collected from the workflow’s environment variables, which is broader than the workflow’s secrets: block would suggest because some workflows export secrets into the environment for use across steps)
  • Cloud credentials: ~/.aws/credentials, ~/.config/gcloud/, ~/.azure/

The exfiltration target is a hardcoded HTTP endpoint plus, in newer Checkmarx-campaign variants, a fallback that uploads the payload to a public GitHub repository under an attacker-controlled account. That bypasses a lot of outbound filters, because blocking unknown endpoints is common and blocking github.com is not.

How many machines were exposed

I do not have a confident number; npm does not publish per-version-per-hour install counts. @bitwarden/cli averages around 80,000 weekly downloads, which puts a naive 93-minute slice around 700 installs. CI traffic is not evenly distributed, and some installs were pinned to older versions. My rough range is 200 to 2,000 exposed developer machines or CI runners. Wide error bars, but the right order of magnitude.

Why this is harder to defend against than the others

The Shai-Hulud waves harvested credentials from packages that developers might or might not have installed. The axios incident hit a package that a lot of developers have but that most developers do not install or upgrade frequently (you typically install it once into a project and then leave it).

The Bitwarden CLI is different. People install it specifically because they care about credential hygiene. They run it on dev machines and in CI to pull secrets out of Bitwarden vaults so the application code does not have to hardcode them. That is a hardening posture; that is a good posture. The compromise turned that posture into the attack surface. The machines that installed @bitwarden/cli are the same machines that had the most credentials worth harvesting.

The 93-minute window is the shortest in this series: faster than axios, pgserve, and the Shai-Hulud waves. Good for the ecosystem. Still bad for any build that ran inside those 93 minutes.

What would have prevented this

The defenses I have been listing in every one of these writeups apply with the usual asymmetry:

  • 2FA on accounts: did not apply, no account was stolen.
  • OIDC trusted publishing: Bitwarden uses it. It worked correctly. The malicious artifact was signed by a legitimate Bitwarden pipeline.
  • Sigstore provenance: present and verified. The package’s provenance shows it was built and signed by Bitwarden’s expected workflow on Bitwarden’s expected repository. That is technically true. The malicious payload was injected before the signing step.
  • npm audit: the advisory was published the next day. During the 93-minute active window, npm audit returned clean.
  • --ignore-scripts: would have stopped the install-time payload. It works for the fraction of installs that had --ignore-scripts=true set globally. The trade-off is real: some packages need install scripts. I still think the default should be scripts off, with explicit exceptions.

The defense I keep coming back to is a release-age cooldown. A four-day hold on @bitwarden/[email protected] means no fresh install of that version reaches a developer machine before npm pulls it. The install fails or resolves to the previous known-good version, depending on the manager’s behavior. By the time the cooldown lifts, the malicious version is no longer on npm.

It does not catch long-burn compromises or already-installed payloads. It does catch the pattern this series keeps hitting: short malicious publish windows. Bitwarden was 93 minutes. axios was three hours. pgserve was roughly five hours. The Asurion malicious update was about 18 hours.

What to do if you ran the install during the window

Per Bitwarden’s guidance and the third-party analyses, treat the host as compromised:

  1. Rotate every npm token, GitHub token, and OAuth token that was on the box. The harvested credentials may already be in use.
  2. Rotate cloud credentials. Check the AWS/GCP/Azure account audit logs for unexpected API calls from unfamiliar IPs in the last 72 hours.
  3. Rotate SSH keys, especially anything used for production access. Revoke the public-key authorization on every host the affected key was authorized for.
  4. Re-pull .env values from your secret manager. Anything that was in a local .env file when the install ran was harvested.
  5. Audit ~/.bash_history / ~/.zsh_history for anything sensitive that a determined attacker could mine (commands containing tokens or passwords that you typed as one-liners, for example).
  6. If running in CI, audit the workflow’s secrets: access logs (GitHub Actions has these) and consider all secrets the affected workflow had access to as exposed.

That is a lot of rotation for a 93-minute exposure. The defender has to do hours of work to recover from a credential harvest that took seconds to execute.

References