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 reportsmbt@1.2.48went 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 therelease-pleaseworkflow 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.jsonand MCP configs,.envfiles and crypto wallets. On Linux GitHub Actions runners it reads the memory of theRunner.Workerprocess 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.jsonSessionStarthook and a.vscode/tasks.jsontask which runs on folder open, both re-running the payload, as “chore: update dependencies” under aclaude@users.noreply.github.comauthor. With aworkflow-scoped token, it additionally adds adependabout/...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 publish | Stolen npm token, and a pushed workflow change that exposed a trusted-publishing token | Trusted publishing reached without changing the publish workflow |
| Install hook | preinstall in the published package | Outside the published package’s own scripts |
| Where data went | Encrypted commits to public repos on the victim’s GitHub account | An 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
preinstallloader to the script it runs, the obfuscatedexecution.js, and refusesmbt@1.2.48and the three@cap-jsversions 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.jswould 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).
- Find the four versions. Search lockfiles for
mbt@1.2.48,@cap-js/sqlite@2.2.2,@cap-js/postgres@2.2.2and@cap-js/db-service@2.10.1. Snyk notes@cap-js/sqlite@2.2.2pulls in@cap-js/db-service@^2.10.0, so the last one can arrive transitively. - Upgrade. Move to
@cap-js/sqlite2.4.0 or later,@cap-js/postgres2.3.0 or later and@cap-js/db-service2.11.0 or later. Formbt, SAP maintainers say 1.2.49 is clean. - Rotate credentials on affected machines and runners: npm and GitHub tokens first, since they let the payload spread, then cloud, Kubernetes and SSH credentials.
- Remove the persistence. Delete
.claude/settings.jsonhooks and.vscode/tasks.jsontasks that runsetup.mjs, the.claude/and.vscode/copies of the payload, anydependabout/...branch andformat-check.ymlworkflow, and repositories described “A Mini Shai-Hulud has Appeared”. Check history for commits byclaude@users.noreply.github.com. - Run CI installs with
--ignore-scriptsand allow lifecycle scripts only where a build needs them. Snyk notes this blocks thepreinstalldelivery. - 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
- StepSecurity, A Mini Shai-Hulud has Appeared, 29 April 2026.
- Snyk, Bun-based stealer hits SAP @cap-js and mbt npm packages.
- Socket, TeamPCP-linked supply chain attack hits SAP CAP and Cloud MTA npm packages, 29 April 2026.
- SAP, GHSA-pvw4-cvr4-97p8 / CVE-2026-46421, cap-js/cds-dbs security advisory.
- SAP maintainers, cap-js/cds-dbs issue #1588 and SAP/cloud-mta-build-tool issue #1224.
- OSV, MAL-2026-3179 (mbt); MAL-2026-3176 to MAL-2026-3178 cover the
@cap-jsversions. - Tenable, Mini Shai-Hulud: frequently asked questions.
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.