Table of Contents
TL;DR #
On June 1, 2026, an attacker abused npm’s GitHub Actions trusted publishing to ship malicious versions of 32 @redhat-cloud-services
packages, 96 versions in total, every one carrying valid npm provenance. The root cause is in the provenance itself: npm binds trusted publishing to a repository plus a workflow filename, not to a branch. The attacker pushed short-lived oidc-<hex>
branches to three RedHatInsights repositories (javascript-clients
, frontend-components
, platform-frontend-ai-toolkit
), and on each branch rewrote the trusted CI workflow into a self-publishing job that ran a Bun worm with id-token: write
. The worm exchanged the workflow’s OIDC token for npm publish tokens, then for each target repackaged the legitimate tarball with a malicious preinstall
hook and republished it, provenance and all. The publishes came in three waves; the first and second waves were later unpublished, but the third wave is still the live latest
for every affected package, so upgrading to the latest patch installs the payload.
The injected preinstall
runs a 4.3 MB index.js
that ROT-9 decodes a , AES-128-GCM decrypts a 634 KB Bun script, downloads the Bun runtime from GitHub, and executes it. The payload scans for AWS, Azure, GCP, HashiCorp Vault, Kubernetes, npm, GitHub, and password manager secrets, exfiltrates them to attacker-created public GitHub repositories, and self-propagates using the stolen credentials.
is the sample analyzed below; the full list of affected packages and versions is in the table at the end of this post.[email protected] Impact:
- Executes on
npm install
before any other code runs, including in CI - Harvests cloud credentials (AWS IMDS, ECS, Secrets Manager, SSM; Azure managed identity; GCP service accounts), Vault tokens, Kubernetes service account tokens, GitHub PATs, npm tokens, and Bitwarden/gopass vaults
- Exchanges GitHub Actions OIDC tokens for npm publish tokens and signs malicious artifacts via Sigstore
- Self-propagates by injecting
.github/workflows/codeql.yml
into accessible repositories and republishing tampered npm tarballs - Attempts Docker socket container escape and installs AI-agent persistence
**Indicators of Compromise (IoC):**
- Package:
`@redhat-cloud-services/`
[[email protected]](/cdn-cgi/l/email-protection) - Tarball SHA256:
031ba872d5a84bfb18115f432811e4b45180346a1bae653f7fd85f918e7bb3a3
index.js
SHA256:df1732f5bfec12e066be44dee02ec8a243e4868d38672c1b1d065359dd735a14
-
Decrypted payload SHA256:
0dc06ecdaa63fe24859cfd955053c23245c536e4733480239d14bebf12688e35 -
Hardcoded AES-128-GCM keys:
fe0d71d57ecf4fa0a433185bf59a03f5
,f5e5dca9b725ec18514c4b322ed35d2b
-
Bun download:
github.com/oven-sh/bun/releases/download/bun-v1.3.13/ -
Runtime artifacts:
/tmp/p<random>.js
,`/tmp/b-<random>/bun`
,`/tmp/kitty-<random>`
- Worm fingerprints: branch
`chore/add-codeql-static-analysis`
, injected.github/workflows/codeql.yml
, pinnedactions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
,.claude/settings.json
and.vscode/tasks.json
persistence - Exfil repo description:
Miasma: The Spreading Blight
- Anti-analysis env vars:
__FAKE_PLATFORM__
,TESTING_TAR_FAKE_PLATFORM
,__IS_DAEMON
,SKIP_DOMAIN
The injection is one line in package.json #
The diff between 4.0.3 and 4.0.4 is small. The attacker added a preinstall
hook and nothing else in the manifest:
preinstall
runs before dependency resolution finishes and before any application code. On a developer laptop or a CI runner, npm install
is enough to trigger the full chain. The main
entry point still points at ./index.js
, the package’s normal barrel file, except that file is no longer a barrel file. In 4.0.3 it is 7.9 KB of Object.defineProperty
re-exports. In 4.0.4 it is 4.3 MB: The attacker removed the original exports and replaced the entire API surface.
Layer 1: ROT-9 over a char-code array #
index.js
is a single statement. It maps a character-code array through String.fromCharCode
, joins the result, Caesar-shifts by 9, and passes everything to eval
:
Decoding the array as data (never executing it) yields an async . It imports node:crypto
and defines an AES-128-GCM helper, then decrypts two embedded blobs with hardcoded keys:
Static decryption of both blobs reproduces the published IoC hashes, confirming the keys:
Layer 2: download Bun, run the payload #
The writes the larger blob (_p
) to a random temp file and runs it under Bun. If Bun is not already present, it first evaluates the smaller blob (_b
), which defines getBunPath()
:
getBunPath()
downloads a pinned Bun release straight from the official GitHub mirror, unzips it to a temp directory, and marks it executable:
The attacker ran the payload under Bun rather than Node. Bun bundles its own TypeScript runtime, fetch, crypto, and shell, so the worm doesn’t touch the victim’s Node installation.
The payload: a multi-cloud credential harvester #
The attacker obfuscated the 634 KB payload with two stacked ciphers. The outer layer uses the obfuscator.io string-array scheme: hex-named variables, a self-rotating string table (2,219 entries, rotated to checksum 0x85d3f
), and a custom base64 alphabet (`abc…xyzABC…XYZ0-9+/`
). The inner layer sits beneath it: a PBKDF2 + SHA-256-keystream S-box cipher installed onto `globalThis["f4abccab2"]`
under a name pulled from the string array at runtime. PBKDF2 derives a 32-byte master key from hardcoded seed P9
and salt N9
at 200,000 iterations; decryption then runs three rounds of per-index SHA-256-keystream S-box substitution with plaintext chaining. Static analysis resolved all 1,577 string-array references and 371 globalThis["f4abccab2"]
calls where the argument is a literal.
Decode the 2,219 string-table entries statically (base64 with the custom alphabet, no execution) and you recover the literal set the payload operates on:
The table also lists the environment variables the worm reads (129 process.env
accesses in total), including NPM_TOKEN
, GITHUB_TOKEN
, CIRCLE_TOKEN
, VAULT_ADDR
, AWS_REGION
, and ANTHROPIC_API_KEY
. AWS access key IDs are matched on the AKIA
prefix. Beyond environment variables, the worm reads ~/.npmrc
, ~/.netrc
, and shell and database history files from disk. It also runs TruffleHog-style regex scans against harvested text: gh[op]_
and npm_
token prefixes, AKIA
AWS key IDs, GCP service account JSON, Azure connection strings, Stripe sk_
/pk_
keys, and database connection strings. The worm targets credentials from approximately 40 CI providers, including CircleCI, Travis CI, Jenkins, GitLab CI, Buildkite, and Vercel.
Self-propagation: npm republish and CI workflow injection #
Two propagation paths sit in the same string table.
For npm, the payload calls the OIDC token exchange and whoami endpoints, repackages a tarball (updateTarball
, package-updated.tgz
), and signs the artifact through Sigstore. Stolen credentials exfiltrate to attacker-created public GitHub repositories, each carrying the description Miasma: The Spreading Blight
. GitHub API calls use a spoofed User-Agent: python-requests/2.31.0
header to blend with typical automation traffic. The string table also contains Fulcio and Rekor hosts alongside provenance schema URLs:
For GitHub, the worm enumerates repositories the token can write to, reads action.yml
/action.yaml
via GraphQL, and commits a workflow through the createCommitOnBranch
mutation so the commit appears as a verified, signed change. The branch name, target file, and pinned Action commit are fixed:
The injected workflow pins actions/checkout
to a specific commit hash rather than a tag, which looks like responsible supply-chain hygiene. A pull request claiming to add CodeQL static analysis reads as a security improvement, the kind of change a maintainer approves without close review.
Container escape, EDR awareness, and AI-agent persistence #
Beyond credential theft, the payload attempts privilege escalation. If it can reach the Docker socket, it launches a container that bind-mounts the host /etc/sudoers.d
and grants the CI runner passwordless sudo:
It checks for endpoint protection before acting, probing for CrowdStrike, SentinelOne, Carbon Black, and StepSecurity Harden-Runner:
It also checks environment variables before executing sensitive operations. If __FAKE_PLATFORM__
, TESTING_TAR_FAKE_PLATFORM
, __IS_DAEMON
, or SKIP_DOMAIN
are set, the payload suppresses specific behaviors. The tripwire keeps the worm quiet inside automated analysis environments while it runs in production.
For persistence, the payload targets developer tooling. The string table includes .claude/settings.json
with a SessionStart
reference and .vscode/tasks.json
, the same AI-agent and editor hijack pattern SafeDep documented in the Mini Shai-Hulud campaign, plus the /tmp/kitty-
daemon path from that toolkit.
Root cause: how malicious versions shipped with valid provenance #
Every malicious version carries a valid npm provenance attestation. The attestation is the artifact that explains the compromise, because it records the exact repository, workflow, commit, and trigger that minted the publish token. Pull the SLSA predicate for the malicious
and compare it to the clean [email protected]4.0.3
:
Same repository, same workflow path, same push
trigger. The only difference is the ref: 4.0.3
was built from refs/heads/main
, 4.0.4
from refs/heads/oidc-4d5900f3
, a branch that no longer exists (git/ref/heads/oidc-4d5900f3
returns 404). The head commit 608d011
is unsigned, persists as a dangling object, and added exactly two files.
The attacker rewrote the publish workflow on a throwaway branch
Diffing ci.yml
at 608d011
against main
shows the entire CI pipeline replaced with a single self-publishing job:
The commit also added _index.js
: a 4.2 MB file with the same try{eval(function(s,n)...
ROT-9 wrapper as the dropper. It is the worm. Run inside the workflow with id-token: write
, it reads the OIDC_PACKAGES
list, exchanges the GitHub Actions OIDC token for an npm publish token through https://registry.npmjs.org/-/npm/v1/oidc/token/exchange/package/
, then for each target downloads the legitimate tarball, injects the preinstall
hook and the dropper index.js
, and republishes with provenance.
Why npm accepted the token
npm GitHub Actions trusted publishing binds trust to repository plus workflow filename, not to a branch, ref, or protected environment. The OIDC certificate minted for the malicious run carries the subject repo:RedHatInsights/javascript-clients:ref:refs/heads/oidc-4d5900f3
and the SAN .../ci.yml@refs/heads/oidc-4d5900f3
. Because the workflow filename (ci.yml
) matched the registered publisher, npm issued the publish right and signed provenance for it. The branch the workflow ran from was never checked. Provenance attests to how a package was built, not that the build was authorized, so npm audit signatures
reports these malicious versions as verified.
Blast radius: three repositories, two waves each
The same pattern repeated across three RedHatInsights repositories, each with its own pair of throwaway oidc-<hex>
branches. Provenance refs from the attestations:
| Repository | Workflow | Branches | Packages |
|---|---|---|---|
`javascript-clients` | `ci.yml` | `oidc-4d5900f3` , `oidc-6523a11b` | 15 (14 `*-client` + `javascript-clients-shared` ) |
frontend-components | ci.yaml | oidc-61fff775 , oidc-af10000d | 14 (chrome , frontend-components* , types , …) |
platform-frontend-ai-toolkit | release.yml | oidc-2530ec68 , oidc-93b9a955 | 3 (hcc-*-mcp ) |
Each repo got two runs roughly three hours apart. The first wave (e.g.
) was unpublished afterward; the second wave (e.g. [[email protected]](/cdn-cgi/l/email-protection)
) bumped the next patch number and remains the live [[email protected]](/cdn-cgi/l/email-protection)`latest`
. Across 32 packages that is 96 malicious versions, and for every package the current latest
on npm is the third-wave payload.
, [[email protected]](/cdn-cgi/l/email-protection)
, and [[email protected]](/cdn-cgi/l/email-protection)
all ship the [[email protected]](/cdn-cgi/l/email-protection)`preinstall`
dropper and a ~4 MB index.js
. Upgrading to the latest patch installs the payload rather than removing it.
Initial access remains the open question
The provenance proves the publish path. It does not prove how the attacker got write access to push branches into three RedHatInsights repositories. The head commits are unsigned and attributed to a real Red Hat engineer (justinorringer
), but git author metadata is forgeable and normal pushes to these repos come from automation (nacho-bot
, platex-rehor-bot
), not that account.
| Ecosystem | Package | Version | |
|---|---|---|---|
| 1 | npm | @redhat-cloud-services/chrome | 2.3.1 |
| 2 | npm | @redhat-cloud-services/compliance-client | 4.0.3 |
| 3 | npm | @redhat-cloud-services/config-manager-client | 5.0.4 |
| 4 | npm | @redhat-cloud-services/entitlements-client | 4.0.11 |
| 5 | npm | @redhat-cloud-services/eslint-config-redhat-cloud-services | 3.2.1 |
| 6 | npm | @redhat-cloud-services/frontend-components | 7.7.2 |
| 7 | npm | @redhat-cloud-services/frontend-components-advisor-components | 3.8.2 |
| 8 | npm | @redhat-cloud-services/frontend-components-config | 6.11.3 |
| 9 | npm | @redhat-cloud-services/frontend-components-config-utilities | 4.11.2 |
| 10 | npm | @redhat-cloud-services/frontend-components-notifications | 6.9.2 |
| 11 | npm | @redhat-cloud-services/frontend-components-remediations | 4.9.2 |
| 12 | npm | @redhat-cloud-services/frontend-components-testing | 1.2.1 |
| 13 | npm | @redhat-cloud-services/frontend-components-translations | 4.4.1 |
| 14 | npm | @redhat-cloud-services/frontend-components-utilities | 7.4.1 |
| 15 | npm | @redhat-cloud-services/hcc-feo-mcp | 0.3.1 |
| 16 | npm | @redhat-cloud-services/hcc-kessel-mcp | 0.3.1 |
| 17 | npm | @redhat-cloud-services/hcc-pf-mcp | 0.6.1 |
| 18 | npm | @redhat-cloud-services/host-inventory-client | 5.0.3 |
| 19 | npm | @redhat-cloud-services/insights-client | 4.0.4 | | 20 | npm | @redhat-cloud-services/integrations-client | 6.0.4 | | 21 | npm | @redhat-cloud-services/javascript-clients-shared | 2.0.8 | | 22 | npm | @redhat-cloud-services/notifications-client | 6.1.4 | | 23 | npm | @redhat-cloud-services/patch-client | 4.0.4 | | 24 | npm | @redhat-cloud-services/quickstarts-client | 4.0.11 | | 25 | npm | @redhat-cloud-services/rbac-client | 9.0.3 | | 26 | npm | @redhat-cloud-services/remediations-client | 4.0.4 | | 27 | npm | @redhat-cloud-services/rule-components | 4.7.2 | | 28 | npm | @redhat-cloud-services/sources-client | 3.0.10 |
| 29 | npm | @redhat-cloud-services/topological-inventory-client | 3.0.10 |
| 30 | npm | @redhat-cloud-services/tsc-transform-imports | 1.2.2 |
| 31 | npm | @redhat-cloud-services/types | 3.6.1 | | 32 | npm | @redhat-cloud-services/vulnerabilities-client | 2.1.8 | | 33 | npm | @redhat-cloud-services/tsc-transform-imports | 1.2.4 | | 34 | npm | @redhat-cloud-services/types | 3.6.2 |
| 35 | npm | @redhat-cloud-services/eslint-config-redhat-cloud-services | 3.2.2 |
| 36 | npm | @redhat-cloud-services/frontend-components-testing | 1.2.2 |
| 37 | npm | @redhat-cloud-services/frontend-components-remediations | 4.9.3 |
| 38 | npm | @redhat-cloud-services/frontend-components-config | 6.11.4 |
| 39 | npm | @redhat-cloud-services/frontend-components-config-utilities | 4.11.3 |
| 40 | npm | @redhat-cloud-services/chrome | 2.3.2 |
| 41 | npm | @redhat-cloud-services/frontend-components-translations | 4.4.2 |
| 42 | npm | @redhat-cloud-services/frontend-components-notifications | 6.9.3 |
| 43 | npm | @redhat-cloud-services/rule-components | 4.7.3 |
| 44 | npm | @redhat-cloud-services/frontend-components-advisor-components | 3.8.4 |
| 45 | npm | @redhat-cloud-services/frontend-components-utilities | 7.4.2 |
| 46 | npm | @redhat-cloud-services/frontend-components | 7.7.3 | | 47 | npm | @redhat-cloud-services/entitlements-client | 4.0.12 | | 48 | npm | @redhat-cloud-services/config-manager-client | 5.0.5 | | 49 | npm | @redhat-cloud-services/quickstarts-client | 4.0.12 | | 50 | npm | @redhat-cloud-services/integrations-client | 6.0.5 | | 51 | npm | @redhat-cloud-services/javascript-clients-shared | 2.0.9 | | 52 | npm | @redhat-cloud-services/notifications-client | 6.1.5 | | 53 | npm | @redhat-cloud-services/patch-client | 4.0.5 | | 54 | npm | @redhat-cloud-services/sources-client | 3.0.11 | | 55 | npm | @redhat-cloud-services/host-inventory-client | 5.0.4 | | 56 | npm | @redhat-cloud-services/vulnerabilities-client | 2.1.9 | | 57 | npm | @redhat-cloud-services/rbac-client | 9.0.4 | | 58 | npm | @redhat-cloud-services/remediations-client | 4.0.5 | | 59 | npm | @redhat-cloud-services/insights-client | 4.0.5 | | 60 | npm | @redhat-cloud-services/compliance-client | 4.0.4 |
| 61 | npm | @redhat-cloud-services/topological-inventory-client | 3.0.11 |
| 62 | npm | @redhat-cloud-services/hcc-kessel-mcp | 0.3.2 |
| 63 | npm | @redhat-cloud-services/hcc-pf-mcp | 0.6.2 |
| 64 | npm | @redhat-cloud-services/hcc-feo-mcp | 0.3.2 |
| 65 | npm | @redhat-cloud-services/tsc-transform-imports | 1.2.6 |
| 66 | npm | @redhat-cloud-services/types | 3.6.4 |
| 67 | npm | @redhat-cloud-services/eslint-config-redhat-cloud-services | 3.2.4 |
| 68 | npm | @redhat-cloud-services/frontend-components-testing | 1.2.4 |
| 69 | npm | @redhat-cloud-services/frontend-components-remediations | 4.9.5 |
| 70 | npm | @redhat-cloud-services/frontend-components-config | 6.11.6 |
| 71 | npm | @redhat-cloud-services/frontend-components-config-utilities | 4.11.5 |
| 72 | npm | @redhat-cloud-services/chrome | 2.3.4 |
| 73 | npm | @redhat-cloud-services/frontend-components-translations | 4.4.4 |
| 74 | npm | @redhat-cloud-services/frontend-components-notifications | 6.9.5 |
| 75 | npm | @redhat-cloud-services/rule-components | 4.7.5 |
| 76 | npm | @redhat-cloud-services/frontend-components-advisor-components | 3.8.6 |
| 77 | npm | @redhat-cloud-services/frontend-components-utilities | 7.4.4 |
| 78 | npm | @redhat-cloud-services/frontend-components | 7.7.5 | | 79 | npm | @redhat-cloud-services/entitlements-client | 4.0.14 | | 80 | npm | @redhat-cloud-services/config-manager-client | 5.0.7 | | 81 | npm | @redhat-cloud-services/quickstarts-client | 4.0.14 | | 82 | npm | @redhat-cloud-services/integrations-client | 6.0.7 | | 83 | npm | @redhat-cloud-services/javascript-clients-shared | 2.0.11 | | 84 | npm | @redhat-cloud-services/notifications-client | 6.1.7 | | 85 | npm | @redhat-cloud-services/patch-client | 4.0.7 | | 86 | npm | @redhat-cloud-services/sources-client | 3.0.13 | | 87 | npm | @redhat-cloud-services/host-inventory-client | 5.0.6 | | 88 | npm | @redhat-cloud-services/vulnerabilities-client | 2.1.11 | | 89 | npm | @redhat-cloud-services/rbac-client | 9.0.6 | | 90 | npm | @redhat-cloud-services/remediations-client | 4.0.7 | | 91 | npm | @redhat-cloud-services/insights-client | 4.0.7 | | 92 | npm | @redhat-cloud-services/compliance-client | 4.0.6 |
| 93 | npm | @redhat-cloud-services/topological-inventory-client | 3.0.13 |
| 94 | npm | @redhat-cloud-services/hcc-kessel-mcp | 0.3.4 |
| 95 | npm | @redhat-cloud-services/hcc-pf-mcp | 0.6.4 |
| 96 | npm | @redhat-cloud-services/hcc-feo-mcp | 0.3.4 |
| No matching rows |
References #
- npm
- oss
- malware
- supply-chain
- shai-hulud
Author
SafeDep Team
safedep.io
Share
The Latest from SafeDep blogs #
Follow for the latest updates and insights on open source security & engineering
Miasma Worm Targets AI Coding Agents via GitHub Repos A Miasma worm variant injects a 4.3 MB dropper into GitHub repos across multiple maintainers, wiring it to auto-run through Claude Code, Gemini, Cursor, and VS Code config files. No npm package is...
Inside MicrosoftSystem64: A Supply Chain RAT Exfiltrating to HuggingFace Deep technical analysis of MicrosoftSystem64, an 81 MB Node.js SEA binary deployed via malicious npm packages. This RAT steals browser credentials, 80+ crypto wallet extensions, Telegram sessions,...
Axios Typosquats Deliver the Epsilon Stealer Two axios typosquats on npm, turbo-axios and faster-axios, form a campaign delivering Epsilon Stealer through a four-stage chain. The Electron infostealer grabs browser credentials, crypto wallets,...
183 npm Packages Target Cloud and Finance via oob.moika.tech Two npm accounts published 164 malicious packages at version 99.99.99 targeting a cloud platform and a financial institution. Both campaigns share identical payload code, the same C2 endpoint, and...
Ship Code. #
Not Malware. #
Start free with open source tools on your machine. Scale to a unified platform for your organization.