Teardown: the SAP CAP and mbt npm compromise

Four SAP npm releases, one preinstall hook, and the first wave of Mini Shai-Hulud.

What happened on 29 April 2026, how the attacker got publish rights to SAP packages, and where Chainsaw would and would not have stopped it.

Questions and answers

What happened?

The SAP CAP compromise was a 29 April 2026 attack in which an attacker published four malicious versions of SAP npm packages: mbt@1.2.48, the Cloud MTA Build Tool, and @cap-js/sqlite@2.2.2, @cap-js/postgres@2.2.2 and @cap-js/db-service@2.10.1 from SAP’s Cloud Application Programming Model (CAP). mbt@1.2.48 went out at 09:55 UTC, and according to Snyk the three @cap-js versions followed between about 11:25 and 12:14 UTC. Snyk puts their combined reach at roughly 52,000 (mbt), 260,000 (@cap-js/db-service), 250,000 (@cap-js/sqlite) and 10,000 (@cap-js/postgres) weekly downloads.

StepSecurity reports detecting the attack within minutes of the first publish and opened issues on both SAP repositories at about 12:03 UTC. SAP maintainers published a clean mbt@1.2.49 at 16:15 UTC the same day and say npm confirmed the removal of 1.2.48 on 30 April. For CAP, SAP shipped superseding versions (@cap-js/sqlite@2.4.0, @cap-js/postgres@2.3.0, @cap-js/db-service@2.11.0) and later confirmed the malicious versions were deleted from the registry. The @cap-js versions are tracked as CVE-2026-46421 and GitHub advisory GHSA-pvw4-cvr4-97p8; all four are in OSV as MAL-2026-3176 to MAL-2026-3179.

How did the attacker get publish rights?

The SAP CAP attacker got publish rights through two different routes, and the public reports are not in full agreement on which package used which.

  • mbt: a long-lived npm token. StepSecurity reports mbt@1.2.48 went out under a static npm automation token belonging to the project’s CI service account, and the package had never used trusted publishing. How the attacker obtained the token is not visible in public data.
  • @cap-js: a hijacked release workflow and trusted publishing. The attacker, according to StepSecurity, used a compromised SAP developer’s GitHub account to push a commit making the release-please workflow run on a non-main branch, and replaced the workflow’s publish steps with an OIDC token exchange printing the resulting npm token, double base64-encoded, into the workflow log. npm’s trusted-publisher setting for the package trusted any branch that ran the release workflow, so the exchange succeeded.

SAP’s own statement, as quoted by Snyk, says an unauthorized actor pushed commits hijacking the release workflow, and “the workflow had publish permissions without any manual approval gate.” Snyk, however, reports all four versions were published from cloudmtabot, the mbt maintainer account, while SAP’s clean releases went out via GitHub Actions trusted publishing, and notes it cannot explain how a hijacked workflow would publish under the cloudmtabot identity. Socket does not say how the versions were published. In both routes the result looked like an ordinary SAP release to anyone checking only who published the package.

What ran on install?

Each SAP package ran a new preinstall hook, node setup.mjs. setup.mjs is a readable loader of about 4.5 KB, byte-identical across all four packages. If the machine has no bun binary, the loader downloads Bun 1.3.13 from GitHub Releases and uses Bun to run execution.js, a single-line file of about 11.6 MB obfuscated with an obfuscator.io-style string table and a custom cipher. For mbt, Snyk reports three new dependencies, axios, tar and unzip-stream, added to support the loader.

According to StepSecurity and Snyk, the payload:

  • Exits on a Russian locale, and forks into a detached background process on developer machines while running inline in CI.
  • Collects credentials: npm and GitHub tokens, AWS, GCP and Azure credentials and secret-manager contents, Kubernetes credentials, password-manager CLI tokens, ~/.claude.json and MCP configs, .env files and crypto wallets. On Linux GitHub Actions runners it reads the memory of the Runner.Worker process to pull out secrets.
  • Exfiltrates through GitHub. It encrypts the results (AES-256-GCM with the key wrapped by an embedded RSA-4096 public key) and commits the results to new public repositories on the victim’s own GitHub account, described “A Mini Shai-Hulud has Appeared”. Snyk counted 1,076 such repositories by 15:17 UTC on 29 April 2026.
  • Persists in repositories. It commits a .claude/settings.json SessionStart hook and a .vscode/tasks.json task which runs on folder open, both re-running the payload, as “chore: update dependencies” under a claude@users.noreply.github.com author. With a workflow-scoped token, it additionally adds a dependabout/... branch with a workflow which dumps the repository’s secrets.
  • Can spread. It validates stolen npm tokens and publishes infected versions of the victim’s other packages straight to the registry API. Snyk had not observed a fifth package carrying the payload.

How does it connect to the later wave?

The SAP CAP compromise is the first wave of Mini Shai-Hulud. Twelve days later the same campaign reached TanStack, covered in our TanStack teardown. Tenable describes Mini Shai-Hulud as active since late April 2026 and counts the campaign as the fourth generation of Shai-Hulud, while StepSecurity calls the SAP wave a third wave; the sources number the generations differently.

Between the two waves the attacker moved further from what defenders inspect:

SAP CAP / mbt (29 April)TanStack (11 May)
Route to publishStolen npm token, and a pushed workflow change that exposed a trusted-publishing tokenTrusted publishing reached without changing the publish workflow
Install hookpreinstall in the published packageOutside the published package’s own scripts
Where data wentEncrypted commits to public repos on the victim’s GitHub accountAn end-to-end encrypted file-sharing network

Researchers attribute the campaign to TeamPCP. Socket assesses the SAP compromise as TeamPCP-linked “with medium confidence”, citing a reused cipher and the same Russian-locale guardrail, and according to Tenable, multiple security firms attribute Mini Shai-Hulud to TeamPCP. StepSecurity and Snyk name no actor.

Would Chainsaw have stopped it?

Yes, on day 0, for SAP package installs through the Chainsaw proxy. The evidence is a re-run of 58 attacks from 2026 against production Chainsaw (v0.22.118) on 19 October 2026:

  • A default rule refuses them on first download. Chainsaw’s rule on install scripts that fetch remote code follows the preinstall loader to the script it runs, the obfuscated execution.js, and refuses mbt@1.2.48 and the three @cap-js versions before any feed lists them. No policy setting has to be turned on.
  • The malware feed makes the refusal permanent. The known-malicious feed Chainsaw carries lists all four versions almost at once, so they stay refused.
  • The day-0 refusal rests on the obfuscation. The loader downloads Bun through an HTTP client library call, which the rule does not count as a remote fetch. What trips the rule is the heavily obfuscated second stage, so the same loader carrying a readable execution.js would not be refused on its bytes; the 48-hour release hold and the feed would stop it.
  • Installs that bypass the proxy get the malware. A laptop or CI runner pointed straight at registry.npmjs.org is not protected. Chainsaw therefore also gates CI, pins laptops through MDM and blocks direct registry traffic at the network.

What should you do?

If mbt@1.2.48 or one of the three @cap-js versions from 29 April 2026 was installed on any machine or CI runner, treat every credential the host could reach as compromised, according to SAP’s advisory. First find the versions (step 1), then rotate npm and GitHub tokens (step 3).

  1. Find the four versions. Search lockfiles for mbt@1.2.48, @cap-js/sqlite@2.2.2, @cap-js/postgres@2.2.2 and @cap-js/db-service@2.10.1. Snyk notes @cap-js/sqlite@2.2.2 pulls in @cap-js/db-service@^2.10.0, so the last one can arrive transitively.
  2. Upgrade. Move to @cap-js/sqlite 2.4.0 or later, @cap-js/postgres 2.3.0 or later and @cap-js/db-service 2.11.0 or later. For mbt, SAP maintainers say 1.2.49 is clean.
  3. Rotate credentials on affected machines and runners: npm and GitHub tokens first, since they let the payload spread, then cloud, Kubernetes and SSH credentials.
  4. Remove the persistence. Delete .claude/settings.json hooks and .vscode/tasks.json tasks that run setup.mjs, the .claude/ and .vscode/ copies of the payload, any dependabout/... branch and format-check.yml workflow, and repositories described “A Mini Shai-Hulud has Appeared”. Check history for commits by claude@users.noreply.github.com.
  5. Run CI installs with --ignore-scripts and allow lifecycle scripts only where a build needs them. Snyk notes this blocks the preinstall delivery.
  6. Tighten publishing. Restrict npm trusted publishing to a specific branch and workflow rather than a whole repository, and put a manual approval gate on release workflows.

Sources

One teardown per notable attack

Get the next teardown by email

What happened, the install-path angle most coverage misses, and the rule that refuses it.