Apr 29, 2026 • 9 min
Mini Shai-Hulud Hits SAP: Four npm Packages Backdoored With a Bun-Powered Credential Harvester
Four packages in the SAP developer ecosystem were backdoored with a preinstall hook that silently fetches the Bun runtime and executes an 11.7 MB credential harvester capable of draining GitHub tokens, cloud secrets, and live CI pipeline secrets from memory.
Overview
The Mini Shai-Hulud supply-chain campaign has reached the SAP developer ecosystem. On April 29, four packages received malicious updates: @cap-js/[email protected], @cap-js/[email protected], @cap-js/[email protected], and [email protected]. These packages are woven into everyday SAP CAP database and Cloud MTA build toolchains, so they reach developer workstations and CI runners that routinely hold GitHub tokens, cloud provider keys, and enterprise deployment credentials.
The delivery mechanism is an npm preinstall hook. Each affected package.json had one line added to its scripts block — "preinstall": "node setup.mjs" — which causes the payload to execute at the start of npm install, before any other work happens. Everything else in the package is untouched: a diff of @cap-js/[email protected] against the clean 2.2.1 release shows no changes to any existing file. The entire compromise is two additions: setup.mjs and execution.js.
Initial Access
The most likely route to the publish credentials is a CircleCI PR build that exposed an npm token. A draft PR titled feat: ci speedup, submitted from gruposbftechrecruiter/harkonnen-navigator-149 against SAP/cloud-mta-build-tool, was opened and closed on April 29 within a matter of minutes. A force-push to that branch erased the commit from GitHub's view — but the CI logs survive independently of the repository. Build pull/1223 on CircleCI checked out a commit that introduced two new files: a Bun loader at bin/config.mjs and an obfuscated blob at bin/mbt.js, and rewired the test command to run both. The job had access to project secrets including CLOUD_MTA_BOT_NPM_TOKEN, CLOUD_MTA_BOT_GITHUB_TOKEN, CircleCI OIDC tokens, and Docker Hub credentials — all shown as redacted in the retained log output. The same logs contain Octokit warnings for POST https://api.github.com/user/repos, a call that aligns with the malware's repository-creation exfiltration path, tying this build to the theft.
First Stage: setup.mjs
setup.mjs is a lightweight bootstrapper whose only job is to get Bun onto the machine and hand off to the real payload. It fingerprints the OS and CPU architecture, fetches Bun 1.3.13 from GitHub Releases if the runtime is not already cached, and uses it to launch execution.js. This two-stage loader pattern — a small, readable bootstrap that pulls a large obfuscated payload — is consistent with the Bitwarden and PyTorch Lightning incidents attributed to the same campaign.
const BUN_VERSION = '1.3.13';
const ENTRY_SCRIPT = 'execution.js';
const url = `https://github.com/oven-sh/bun/releases/download/bun-v${BUN_VERSION}/${asset}.zip`;
execFileSync(binPath, [entryScriptPath], { stdio: 'inherit', cwd: SCRIPT_DIR });
Second Stage: execution.js
At 11.7 MB, execution.js is a single monolithic payload protected by a custom obfuscation layer identified internally as ctf-scramble-v2. Before doing anything, it probes its environment: it terminates early when the system locale is set to Russian, behaves differently depending on whether it detects a CI context, and forks itself into the background on developer machines so it survives the install session. The credential sweep covers a wide surface — GitHub tokens (queried directly via the gh CLI as well as read from config files), npm tokens from .npmrc, environment variables, cloud provider secrets across AWS, Azure, and GCP, Kubernetes service account tokens, and local tooling configs including Claude, MCP, Signal, Electrum, and VPN clients.
On CI runners the payload goes a step further. A bundled Python component walks the /proc filesystem to locate the GitHub Actions Runner.Worker process and reads its memory directly, pulling out masked secrets that would never appear in logs or environment dumps. Collected data is compressed, encrypted with AES-256-GCM, and the encryption key is sealed with a hardcoded RSA public key before anything leaves the machine. Results land in freshly created public GitHub repositories with randomised Dune-themed names, written as results/results-<timestamp>-<counter>.json, all under the repository description A Mini Shai-Hulud has Appeared.
Propagation
Beyond credential theft, the malware attempts to spread using the access it has just stolen. It queries the GitHub commit search API for the marker string OhNoWhatsGoingOnWithGitHub and treats any matching commit message as a base64-encoded token to try. This lets different infected machines pool tokens through a shared public channel without any direct C2 infrastructure. Where those tokens grant sufficient permissions, the payload rewrites npm package tarballs: it inserts setup.mjs and execution.js, wires in the preinstall hook, increments the patch version, and re-packs the archive, continuing the infection chain to the next consumer of that package. It also seeds files directly into repositories — .vscode/setup.mjs, .claude/execution.js, and .claude/settings.json — using commits attributed to claude <[email protected]> with the message chore: update dependencies, designed to blend into normal repository activity.
Remediation
If any affected version was installed, treat the scope of compromise as broad. The payload sweeps GitHub, npm, cloud providers, Kubernetes, CI pipelines, and local developer tooling in a single pass — limiting rotation to npm tokens will leave other credentials exposed. Search recently created public GitHub repositories for the description A Mini Shai-Hulud has Appeared, scan commit history for the propagation markers in the IOCs below, and check for unexpected .claude/ or .vscode/setup.mjs files in repositories the affected machine had write access to.
IOCs
Affected packages
@cap-js/[email protected] · @cap-js/[email protected] · @cap-js/[email protected] · [email protected]
File hashes (@cap-js/[email protected])
setup.mjs 4066781fa830224c8bbcc3aa005a396657f9c8f9016f9a64ad44a9d7f5f45e34
execution.js 6f933d00b7d05678eb43c90963a80b8947c4ae6830182f89df31da9f568fea95
runner memory dumper 29ac906c8bd801dfe1cb39596197df49f80fff2270b3e7fbab52278c24e4f1a7
Strings & markers
A Mini Shai-Hulud has Appeared
OhNoWhatsGoingOnWithGitHub
ctf-scramble-v2 · tmp.987654321.lock
chore: update dependencies · [email protected]
URLs
hxxps://github[.]com/oven-sh/bun/releases/download/bun-v1.3.13/
hxxps://api.github[.]com/search/commits?q=OhNoWhatsGoingOnWithGitHub
hxxp://169.254.169.254 · hxxp://169.254.170.2 · hxxp://[fd00:ec2::254]