cd /news/ai-safety/mini-shai-hulud-miasma-the-spreading… · home topics ai-safety article
[ARTICLE · art-21951] src=safedep.io pub= topic=ai-safety verified=true sentiment=↓ negative

Mini Shai-Hulud "Miasma: The Spreading Blight" Hits @redhat-cloud-services: Multiple Packages at Risk

On June 1, 2026, an attacker exploited npm's trusted publishing mechanism to compromise 32 @redhat-cloud-services packages across 96 versions, injecting malicious preinstall hooks that execute a Bun-based worm upon npm install. The worm harvests cloud credentials, vault tokens, Kubernetes service account tokens, and other secrets, exfiltrating them to attacker-controlled GitHub repositories while self-propagating through injected CI workflows. The third wave of malicious publishes remains live as the latest versions, meaning any user upgrading to the current patch installs the payload.

read14 min publishedJun 1, 2026

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.

── more in #ai-safety 4 stories · sorted by recency
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/mini-shai-hulud-mias…] indexed:0 read:14min 2026-06-01 ·