"A Mini Shai-Hulud Has Appeared": Bun-Based Stealer Hits SAP @cap-js and mbt npm Packages
April 29, 2026
0 mins readOn April 29, 2026, attackers published malicious versions of four npm packages in the SAP development ecosystem: mbt, @cap-js/db-service, @cap-js/sqlite, and @cap-js/postgres. Each compromised release ships a preinstall hook that downloads the Bun JavaScript runtime from GitHub Releases and uses it to execute an ~11.6 MB obfuscated credential stealer.
The payload tags itself with a hardcoded description, "A Mini Shai-Hulud has Appeared", which is appearing in public GitHub search results in real time as compromised developer machines create dead-drop repositories on their own accounts. The campaign reuses the Shai-Hulud name (the dead-drop repositories are tagged with it) and includes functional npm self-propagation code per StepSecurity's full deobfuscation. As of publication, only the four originally compromised packages have been observed in the wild; the technical breakdown and the compromised npm account that enabled the initial publishes are detailed below.
Snyk has published advisories for all four compromised versions, and the affected releases are flagged by snyk test and shown on each package's Snyk Security Database page.
Affected packages and Snyk advisories
Package | Compromised version | Snyk advisory | Approx. weekly downloads |
|---|---|---|---|
|
| ~52,000 | |
|
| ~260,000 | |
|
| ~250,000 | |
|
| ~10,000 |
Weekly download counts from the npm registry's public downloads API for the week preceding compromise (April 22–28, 2026). All four packages are part of the SAP Cloud Application Programming Model toolchain. mbt is the npm-distributed Cloud MTA Build Tool used to build deployment archives for SAP cloud applications, and the @cap-js/* packages provide database services for CAP applications.
The malicious versions were published in a tight window on April 29, 2026, all UTC, with timestamps verified directly from registry.npmjs.org and the GitHub API:
09:55:25:mbt@1.2.48published from thecloudmtabotnpm account.10:01:07: first victim dead-drop repository appears on GitHub (gruposbftechrecruiter/siridar-navigator-935, per GitHub API timestamps).11:25:47:@cap-js/sqlite@2.2.2published.12:03 to 12:04: StepSecurity files disclosure issues oncap-js/cds-dbs#1588andSAP/cloud-mta-build-tool#1224. An independent third disclosure,SAP/open-ux-tools#4616bylongieirl, is filed at 14:02.12:14:00:@cap-js/postgres@2.2.2and@cap-js/db-service@2.10.1published.13:31: cap-js maintainerchgeoresponds: "Thanks for lettings us know. We are about to clean up the packages with highest priority and investigate the root causes."13:46: SAP publishes clean post-incident versions via thecap-npmGitHub Actions OIDC trusted publisher:@cap-js/db-service@2.11.0,@cap-js/sqlite@2.4.0,@cap-js/postgres@2.3.0(with@cap-js/hana@2.8.0as a coordinated bump).14:02:50: SAP engineerpatricebenderfilescap-js/cds-dbsPR #1592, gating npm publish behind a manual approval environment, with the verbatim root-cause statement quoted above.14:24:cloud-mta-build-toolcollaboratorkbarnoldresponds: "Thank you for the report. We're working on it with high priority."
@cap-js/sqlite@2.2.2 was unpublished from npm shortly after detection. The remaining malicious versions carry npm deprecation strings: mbt@1.2.48 is flagged with "SECURITY: This version contains malicious code. Do not use." while the @cap-js/* malicious versions read "DO NOT USE. This version contains unknown content." Critically, mbt@1.2.48 was still the latest dist-tag for mbt at the time of writing, with no remediated mbt version published yet, meaning a bare npm install mbt still resolves to the malicious tarball. No CVE, GHSA, or OSV record had been assigned for any of the four packages at publication time; the only authoritative public sources are the four vendor blogs cited throughout this post and the three GitHub disclosure issues.
Why this is called "Mini" Shai-Hulud (and what's actually different)
The Shai-Hulud naming convention has appeared in npm supply chain incidents twice before. The original Shai-Hulud campaign in September 2025 hit @ctrl/tinycolor, ngx-bootstrap, ng2-file-upload, and a long tail of dependents (Snyk's zero-day vulnerability report tracked it as it unfolded). The follow-on wave SHA1-Hulud in November 2025 expanded to over 600 distinct packages, including releases from Zapier, PostHog, and Postman. Kaspersky's Securelist team has published independent technical analyses of both prior waves (detection name HEUR:Worm.Script.Shulud.gen) at securelist.com/shai-hulud-worm-infects-500-npm-packages and securelist.com/shai-hulud-2-0.

Shai-Hulud NPM Attack: Remediation with Snyk (short walkthrough on identifying and remediating Shai-Hulud-affected dependencies in your projects using Snyk's CLI and advisor pages).
Both prior campaigns demonstrated worm behavior: the payload would harvest a victim's npm token, then use it to publish itself into every other package the token had write access to. Public attention has tracked accordingly: the Wikipedia article on the Dune sandworm peaked at 2,772 views during the November 2025 SHA1-Hulud disclosure (the highest single-day count in a 16-month dataset, roughly four times the article's average), and the broader "supply chain attack" Wikipedia article reached its highest monthly average on record in April 2026, the month of Mini Shai-Hulud (310 views/day, per Wikimedia REST API).
The April 29 campaign reuses the Shai-Hulud branding (the dead-drop repositories are named with the description string) but the public evidence so far points to a narrower set of behaviors:
Credential theft: observed and detailed by multiple researchers.
Persistence injection into developer tooling configs: novel and detailed below.
Autonomous npm self-publishing: the code is present and functional per StepSecurity's static deobfuscation. The payload harvests npm tokens with regex
/npm_[A-Za-z0-9]{36,}/g, validates each one againstregistry.npmjs.org/-/npm/v1/tokens(filtering forbypass_2fa: trueplus org-level write scope), enumerates accessible packages, patchessetup.mjsandexecution.jsinto a tarball copy, and publishes via a directPUTto the npm registry without invoking the npm CLI. As of publication, no fifth package outside the original four has been observed in the wild carrying the malicious payload, but the worm capability is an empirical fact, not an inference.CI-pipeline hijack as the access vector: SAP's own first-party statement in PR #1592 by
patricebenderoncap-js/cds-dbs(filed 14:02 UTC the same day) reads: "On April 29, 2026, a supply chain attack compromised the repository — an unauthorized actor pushed malicious commits that hijacked the release workflow and triggered unauthorized npm publications. The attacker was able to publish compromised packages because the workflow had publish permissions without any manual approval gate." The fix is to gate npm publish behind an environment review. Per npm registry data, all four malicious versions were published from thecloudmtabotaccount (the legitimatembtmaintainer); SAP's clean post-incident versions at 13:46 UTC were published via thecap-npmGitHub Actions OIDC trusted publisher, not bycloudmtabot. Thecloudmtabotaccount itself has since been suspended on npm. Maintainer-credential compromise is the recurring entry point for this class of incident, but the SAP statement specifically points to the workflow's missing approval gate as the structural root cause.
The credential theft enables npm self-replication, but the immediately observed activity is exfiltration and a new persistence mechanism. Defensive priorities (lockfile audit, credential rotation, lifecycle script policy) are the same either way.
How the attack works
The compromise pattern is consistent across all four packages: the malicious tarball preserves the legitimate package files unchanged (so the CLI still works after install) and adds two new files, setup.mjs (a 4.5 KB plaintext Bun loader, byte-identical across all four packages) and execution.js (an obfuscated payload at exactly 11,678,349 bytes, with hashes that differ between mbt and the @cap-js/* packages). For mbt only, the malicious package.json also adds three dependencies (axios, tar, unzip-stream) that don't appear in the prior clean release; the three cap-js packages added only the preinstall hook to their existing package.json.
Fetching a small loader at install time and using it to pull in a much larger runtime and payload is a pattern Snyk has seen elsewhere this year, including the axios cross-platform RAT incident where the install hook reached out for a native binary delivered through a separate dependency.
For mbt, the diff between 1.2.47 (clean) and 1.2.48 (malicious) looks like this:
preinstall runs before npm reports any output to the user and before any conditional --ignore-scripts policy can intervene if the flag isn't set. The hook executes setup.mjs, which:
Detects platform and architecture (including Alpine/musl detection on Linux).
Checks whether
bunis already onPATHviahasCommand("bun"). If present, skips the download step and uses the existing binary. Otherwise downloads Bun1.3.13from GitHub Releases (following HTTP redirects without destination validation, per Socket's analysis).Extracts the Bun binary to a temporary directory if downloaded.
Invokes
bun execution.jsto run the obfuscated payload.
Once bun execution.js fires, the payload runs through two layers of obfuscation: a string-table rotation in the obfuscator.io style (48,370 entries decoded against a non-standard base64 alphabet and gated by a startup checksum) and a custom cipher StepSecurity dubbed "ctf-scramble-v2" with a PBKDF2-derived master key. Both layers were fully recovered statically. The unwrapped runtime is JavaScript executing under Bun (no embedded WASM or native code; the payload uses Bun-native APIs like Bun.gunzipSync() and Bun.main).
StepSecurity's static deobfuscation describes the following payload behaviors:
Harvests local credentials, GitHub and npm tokens, GitHub Actions secrets, cloud secrets across AWS, Azure, GCP, and Kubernetes, password manager CLI tokens (1Password, Bitwarden, LastPass),
~/.claude.jsonand MCP server configs, environment variables matchingKEY/TOKEN/SECRET/PASSWORDpatterns, and crypto wallet keys present on the host. The payload also queries the AWS instance metadata service (169.254.169.254) when reachable.On Linux CI runners, it spawns a Python child process that reads
/proc/{pid}/memof the GitHub ActionsRunner.Workerprocess to dump plaintext secrets directly from runner memory.On Windows, invokes PowerShell with
-ExecutionPolicy Bypass.Serializes the stolen data as JSON, gzip-compresses it, encrypts it with AES-256-GCM (32-byte key, 12-byte IV), and wraps the AES key using RSA-OAEP-SHA256 against an embedded RSA-4096 public key (stored as gzip+base64 PEM at file offset 9,429,992 in
execution.js, decompressed at runtime viaBun.gunzipSync()). This means defenders who discover the dead-drop repository can see that something was stolen, but cannot determine what. Effectively, every credential the process could touch should be treated as exposed.Creates a public repository on the victim's GitHub account, named with random characters and tagged with the description "A Mini Shai-Hulud has Appeared", and commits the encrypted ciphertext as the dead drop.
Checks the system locale via JavaScript's
IntlAPI and POSIX environment variables (LC_ALL,LC_MESSAGES,LANGUAGE,LANG). If a Russian (ru) locale is detected, the payload logs"Exiting as russian language detected!"and terminates. CIS-region exemption checks align with documented behavior of cybercriminal operations in those regions.
The persistence mechanism
Whether the campaign is classified as a worm, the persistence mechanism is the most novel element documented in public reporting.
According to StepSecurity, the payload uses three persistence and re-execution mechanisms:
Developer-tool config injection. Into every GitHub repository the stolen token can write to, the payload commits a
.claude/settings.jsonfile that uses Claude Code'sSessionStarthook to re-execute the payload whenever a developer starts a Claude Code session, and a.vscode/tasks.jsonwith"runOn": "folderOpen"that triggers when the project is opened in VS Code. These injected files are committed under the identityclaude@users.noreply.github.comwith the message"chore: update dependencies", which can resemble routine automation commits on PR review.GitHub Actions workflow injection. When the stolen token has
workflowscope, the payload creates a typosquatted branchdependabout/github_actions/format/setup-formatter(note the missingt) and commits.github/workflows/format-check.ymlimpersonating the Dependabot service account (dependabot[bot]@users.noreply.github.com, commit message"Add formatter workflow"). The injected workflow uses${{ toJSON(secrets) }}to dump every repository secret into a build artifact namedformat-results.txt.Daemonization on developer machines. On non-CI hosts, the payload forks itself as a detached background process tagged with the env var
__DAEMONIZED=1, then the parent exits cleanly sonpm installreturns to the user with no visible error. The detached child continues running with no clear process ancestry.
This extends the set of installation-side persistence vectors observed in npm supply chain campaigns to include AI coding agent configuration files, alongside package.json lifecycle hooks, CI workflows, and IDE plugin manifests.
The AI-agent configuration attack surface is documented prior art rather than a first appearance. Microsoft's VSCode #309406 on the tasks.json runOn: folderOpen vector was filed sixteen days earlier and closed as By Design (Workspace Trust is treated as sufficient mitigation, though the trust check does not cover commits made into already-trusted repos). Anthropic's claude-code #49778 on the SessionStart hook was filed twelve days earlier and remains open without an Anthropic response, citing a real-world precedent in the Cozempic supply chain audit. The Trivy AI-agent compromise (CVE-2026-28353) in March 2026 went further, using stolen tokens to publish a weaponized VS Code extension that targeted five different AI coding agents. Snyk has covered this same crossover separately in the Clinejection writeup. Mini Shai-Hulud is the next data point in that pattern, not the first.
Indicators of compromise
This list is condensed from public reporting by StepSecurity, Aikido, Socket, and SafeDep. Refer to those reports for full IOCs and forensic detail. Each malicious version remains queryable as an immutable record on the npm registry at registry.npmjs.org/mbt/1.2.48, registry.npmjs.org/@cap-js/sqlite/2.2.2, registry.npmjs.org/@cap-js/postgres/2.2.2, and registry.npmjs.org/@cap-js/db-service/2.10.1; the publish timestamp and "preinstall": "node setup.mjs" fingerprint are preserved in each record.
Live GitHub searches for the two distinctive strings:
Dead-drop repo description: https://github.com/search?q=%22A+Mini+Shai-Hulud+has+Appeared%22&type=repositories (returning 1,076 repositories at 15:17 UTC on April 29, per direct GitHub API count, with new repos appearing in real time)
P2P token dead-drop commit string: https://github.com/search?q=OhNoWhatsGoingOnWithGitHub&type=commits
Each dead-drop repo contains a single README.md plus one or more files at results/results-<unix-ms>-<counter>.json. Direct inspection of one live victim repo by external researchers confirmed the file format:
Because the wrapping key is the attacker's RSA-4096 public key, the contents are unreadable to defenders even with full access to the victim's GitHub account.
What to do
The blast radius depends on whether your build pipelines or developer machines ran npm install against any of the four affected versions during the exposure window (roughly 10:00 to 14:00 UTC on April 29, 2026, plus any time the deprecated versions remain resolvable).
Check your installs. Search lockfiles for the affected versions, and watch the transitive resolution: @cap-js/sqlite@2.2.2 declares @cap-js/db-service@^2.10.0 as a dependency, so a clean install of @cap-js/sqlite from a reachable version range can pull @cap-js/db-service@2.10.1 (the malicious version) as a transitive without anyone listing it directly.
For Snyk customers, snyk test and snyk monitor will surface the malicious versions against the four advisories listed above. The Snyk Security Database pages for mbt, @cap-js/db-service, @cap-js/sqlite, and @cap-js/postgres reflect the security incident as well.
If a compromised version was installed:
Treat every credential reachable from that machine or pipeline as exposed. The attacker-controlled RSA encryption means defenders cannot read the dead-drop contents to confirm scope, so rotation has to be conservative. The 134 file-path patterns StepSecurity recovered from the payload include, at minimum: npm publish tokens (highest priority, since these enable worm-style propagation across other packages a developer maintains), GitHub PATs and OAuth grants, AWS/Azure/GCP credentials and any roles assumable from the affected machine, Kubernetes kubeconfigs (
~/.kube/config, k3s YAML, in-pod service-account tokens), Docker configs (~/.docker/config.json), Terraform Cloud credentials (~/.terraform.d/credentials.tfrc.json), Helm config, SSH keys (~/.ssh/id*,config,known_hosts, host keys under/etc/ssh/), password manager CLI tokens (1Password'sop, Bitwarden'sbw, LastPass),.npmrc/.pypirc/.yarnrc,.gitconfigand.git-credentials, shell history files, environment variables and.env*files matching*_TOKEN,*_KEY,*_SECRET, or*_PASSWORDpatterns,~/.claude.jsonand any MCP server configs (including Kiro MCP configs at~/.kiro/settings/mcp.json), messaging app session data (Signal, Slack cookies, Discord, Telegram, Element, Pidgin), VPN configs (NordVPN, ProtonVPN, OpenVPN, etc.), FileZilla server lists, Ansible configs, and crypto wallet keys for Bitcoin, Ethereum, Monero, Dash, Zcash, Dogecoin, Litecoin, Electrum, Exodus, Ledger Live, and Atomic. The payload also reads AWS instance metadata (169.254.169.254,169.254.170.2,fd00:ec2::254) and ECS task metadata, so any IAM roles reachable from those endpoints should be considered exposed.Audit GitHub Actions secret exposure. The
/proc/{pid}/memextraction technique readsRunner.Worker's full readable address space, so any secret used in the workflow up to that point is in scope, even if not directly referenced by the install step. Default GitHub-hosted runners do not block this access; detection requires a runner-side security agent like StepSecurity Harden-Runner.Sweep your GitHub org for the IOCs in the code block above: dead-drop repos matching the Dune-themed naming pattern with the "Mini Shai-Hulud" description, the
claude@users.noreply.github.comauthor signature, thedependabot[bot]@users.noreply.github.comimpersonation on adependabout/...branch, the injected.github/workflows/format-check.ymlworkflow that exfiltratestoJSON(secrets)to aformat-results.txtartifact, theOhNoWhatsGoingOnWithGitHubP2P token-dead-drop commit string, and unexpected.claude/settings.jsonor.vscode/tasks.jsonadditions.Pin to clean versions. For the three
@cap-jspackages, prefer SAP's post-incident releases (@cap-js/db-service@2.11.0,@cap-js/sqlite@2.4.0,@cap-js/postgres@2.3.0) as the forward pin and the pre-incident versions (2.10.0,2.2.1,2.2.1respectively) as the minimum floor. Formbt, no remediated version had been published at the time of writing; pin tombt@1.2.47until SAP ships a successor.
General hardening:
For CI environments, default to
npm install --ignore-scriptsand explicitly allow lifecycle scripts only for packages that genuinely require them. This neutralizes the entire class ofpreinstall-driven credential theft, which has now been the delivery mechanism for the original Shai-Hulud, SHA1-Hulud, and this campaign. The lifecycle-hook attack vector itself was reported to npm in 2016 and marked Working As Intended, as paulirish reflagged on Hacker News; a decade later, it remains the most-exploited surface in the ecosystem. Snyk has more on this in NPM Security Best Practices and a longer treatment of upstream defenses in How to prevent malicious packages.Consider package managers that are secure-by-default for lifecycle scripts. pnpm v10 blocks lifecycle scripts unless explicitly allowed, and Bun-as-installer ships with a default trusted-package allowlist. The latter detail is sharp here: the malware uses Bun-as-runtime to execute its payload, but Bun-as-installer would have blocked the
preinstallhook that delivers it. The Bun community is also tracking aminimumReleaseAgeconfiguration request (#28729) for cooldown-style mitigation, in line with Yossarian's case for dependency cooldowns.Watch for unexpected
.claude/settings.jsonand.vscode/tasks.jsonchanges in PR diffs. Treat additions in either file as a potential supply chain signal, even if they look like routine dependency hygiene commits.For SOC and detection engineering teams, the Microsoft Defender KQL queries published in
m4nbat/100_days_of_kql_2026(Day 17) catch the Bun + TruffleHog +/proc/memchain that this campaign reuses; they were authored for SHA1-Hulud and apply to Mini Shai-Hulud without modification. The Wiz Research IOC feed andgensecaihq/Shai-Hulud-2.0-Detectorare useful blocklists once the maintainers update them with the four new packages.Use risk-based prioritization to focus rotation and triage on the credentials with the highest blast radius. Not every secret in scope is equally sensitive, and the encrypted dead-drop means defenders are working with incomplete information.
Start securing your Python apps
Find and fix Python vulnerabilities with Snyk for free.
No credit card required.
Or Sign up with Azure AD Docker ID Bitbucket
By using Snyk, you agree to abide by our policies, including our Terms of Service and Privacy Policy.
