# Miasma npm worm teardown: Red Hat, binding.gyp and AsyncAPI

> How the Miasma worm hit 32 @redhat-cloud-services packages, ran through binding.gyp and then AsyncAPI imports, and whether Chainsaw would have stopped it.

Source: https://chain305.com/teardowns/miasma-npm-worm/

---

# Teardown: the Miasma npm worm

A preinstall hook, then binding.gyp, then import time: one malware family, three ways in.

What happened between 1 June and 14 July 2026, how each wave reached npm with provenance attached, and where Chainsaw would and would not have stopped it.

By [Muhammad Zeeshan Sultan](https://chain305.com/about/#founder), founder of Chain305 · October 2026

Questions and answers

## What happened?

On 1 June 2026, 96 malicious versions of 32 `@redhat-cloud-services` npm packages were published with valid provenance, carrying a credential-stealing worm its own exfiltration repositories call “Miasma: The Spreading Blight”. Wiz dates the malicious commits behind them to about 10:53 and 13:44 UTC; SafeDep counts three publishing waves, the first two later unpublished. SafeDep gives 96 versions, while Wiz’s table lists about 90.

The same malware family came back twice:

Wave

When

Packages

Code ran from

Red Hat

1 June 2026

32 `@redhat-cloud-services` packages, 96 versions

A new `preinstall` hook

Phantom Gyp

3 June 2026, from 23:30 UTC

57 packages, 286+ versions, starting with `@vapi-ai/server-sdk`

A 157-byte `binding.gyp`

AsyncAPI

14 July 2026

`@asyncapi/generator`, `generator-helpers`, `generator-components` and `specs`, 5 versions

Module-scope code, run when the module loads

Researchers do not agree on who is behind it. Wiz says the tradecraft is consistent with TeamPCP’s Mini Shai-Hulud campaign and that the payload is derived from the open-sourced Mini Shai-Hulud code, but calls this overlap rather than attribution and does not rule out a copycat. SafeDep links it to Mini Shai-Hulud through shared persistence patterns. JFrog names no actor for the AsyncAPI wave.

## How did it spread?

Through each victim’s own trusted-publishing setup, so the malicious releases carried npm provenance that pointed at the real repositories. How the attacker first got write access to the Red Hat repositories is disputed:

-   **Wiz** says attackers compromised a Red Hat employee’s GitHub account and used it to push orphan commits that added a `release` workflow with `id-token: write`.
-   **SafeDep** describes short-lived `oidc-<hex>` branches whose rewritten workflow published to npm, and calls the original write access “the open question”.
-   **The OSV entry** describes it as a compromise of the GitHub Actions OIDC trusted publisher shared by the packages.

The payload then spreads on its own. According to SafeDep and the OSV entry, it uses stolen npm credentials and GitHub Actions OIDC tokens to publish tampered tarballs of other packages, signs them through Sigstore, and commits a `.github/workflows/codeql.yml` to a `chore/add-codeql-static-analysis` branch in repositories it can write to. StepSecurity reports the Phantom Gyp wave doing the same with stolen npm tokens, and injecting the same logic into RubyGems through `extconf.rb`.

The AsyncAPI wave arrived differently. JFrog reports an unsigned commit pushed straight to the repository’s `next` branch, which triggered AsyncAPI’s real `release-with-changesets.yml` workflow and published through its OIDC trusted publisher.

## What ran on install?

In the first Miasma wave, on 1 June 2026, a `preinstall` hook in the `@redhat-cloud-services` packages ran `node index.js`, a 4.29 MB obfuscated file where the clean release had 7.9 KB. SafeDep and the OSV entry describe a ROT-9 `eval` wrapper over an AES-128-GCM-encrypted payload, which downloads Bun v1.3.13 from GitHub if Bun is missing and runs the worm under Bun, outside the victim’s Node installation. The worm collects:

-   AWS (instance metadata, ECS, Secrets Manager, SSM), Azure managed identity and GCP service-account credentials
-   HashiCorp Vault and Kubernetes service-account tokens
-   npm tokens, GitHub tokens, `~/.npmrc`, `~/.netrc`, shell and database history
-   secrets from about 40 CI systems, and Bitwarden and gopass stores

Stolen data goes to attacker-created public GitHub repositories; if the Docker socket is reachable, the worm escapes the container and grants the runner user passwordless sudo. The worm persists through `.claude/settings.json` and `.vscode/tasks.json`.

The Phantom Gyp wave declared no `preinstall` or `postinstall` script. StepSecurity reports that npm sees the root `binding.gyp`, runs `node-gyp rebuild`, and the file’s `<!(...)` command substitution runs a 4.5 MB `index.js`, so a check that only looks for `preinstall` or `postinstall` sees nothing.

The AsyncAPI wave did not run on install either, according to JFrog: about 7.7 KB of obfuscated code, hidden after hundreds of spaces on one line, runs when the module is loaded, spawns a hidden `node -e` process and fetches `sync.js` from IPFS. The OSV entry says the code runs on install. Sources also disagree on what the code does: JFrog finds a remote-access trojan with credential theft and propagation switched off in the AsyncAPI build, while the OSV entry says it harvests browser credentials, SSH keys, npm tokens and wallets.

## Would Chainsaw have stopped it?

Yes for the Red Hat wave of the Miasma worm: the `@redhat-cloud-services` versions are refused on day 0, for installs that go through the Chainsaw proxy. The AsyncAPI wave rests on the release hold and the malware feed, and the result makes no claim for the Phantom Gyp wave. We measured this in our re-run of 58 attacks from 2026 against production Chainsaw (v0.22.118) on 19 October 2026:

-   **The Red Hat versions are refused on first download.** The default policy rule “Block install scripts that fetch remote URLs” refuses the Red Hat versions on first download: the `preinstall` hook decodes a value and passes the value to `eval`. Chainsaw reads the package the first time the package is fetched, so the install fails before the hook runs. No policy change and no malware feed listing is needed.
-   **The malware feed lists all 96 Red Hat versions**, so the versions stay refused.
-   **The day-0 refusal rests on one signal near the end of the 4 MB obfuscated loader.** A variant packed differently may not trigger the signal.
-   **The AsyncAPI versions are not refused on their bytes on day 0.** The code runs at import, with no install hook, and the re-run could not confirm that Chainsaw’s default import-time rule catches the code. The AsyncAPI versions rely on the release hold and the malware feed.
-   **Installs that bypass the proxy are not protected.** A laptop or CI runner pointed straight at registry.npmjs.org gets the package. This is why Chainsaw also gates CI, pins laptops through MDM and blocks direct registry traffic at the network.

## What should you do?

The Miasma worm shipped in 96 malicious versions of 32 `@redhat-cloud-services` npm packages on 1 June 2026, then returned in the Phantom Gyp wave from 3 June 2026 and in five versions of four AsyncAPI packages on 14 July 2026. If you installed an affected `@redhat-cloud-services` version, the first step is the one set by GitHub’s advisory (via OSV): treat the machine as fully compromised and rotate every secret the machine held from a different computer. Then:

1.  **Check GitHub for the worm’s traces.** Wiz advises reviewing for unexpected repositories (described “Miasma: The Spreading Blight”), new access tokens and unusual workflow runs. SafeDep lists `oidc-<hex>` and `chore/add-codeql-static-analysis` branches as indicators.
2.  **Look for injected editor and AI-assistant config.** StepSecurity advises searching repositories for injected files such as `.claude/setup.mjs`, `.cursor/rules/setup.mdc` and `.vscode/tasks.json` and removing commits made by automated tokens.
3.  **Block the install-time paths where you can.** StepSecurity recommends `npm install --ignore-scripts`, blocking automatic `node-gyp` builds, lockfiles with integrity hashes, and flagging an oversized root `index.js` that is not the package’s declared entry point.
4.  **For AsyncAPI, check whether the module loaded, not just whether it is installed.** JFrog advises looking for a detached `node -e` child and `sync.js` in a `NodeJS` folder, removing `sync.js`, the `.miasma` folder and `miasma-monitor` persistence, checking shell startup files for a `### Node Auto-Update Script ###` block, and blocking the C2 address and IPFS CID JFrog lists. By JFrog’s analysis nothing runs at install, so `--ignore-scripts` does not help here.
5.  **Protect the branches that trigger releases.** JFrog advises reviewing branch protection, required approvals, workflow permissions and npm trusted-publisher configuration: provenance shows which pipeline published a release, not whether the commit behind it is authorised.

## Sources

-   Wiz, [Miasma supply chain attack targeting Red Hat npm packages](https://www.wiz.io/blog/miasma-supply-chain-attack-targeting-redhat-npm-packages), June 2026.
-   SafeDep, [@redhat-cloud-services hit by Mini Shai-Hulud npm worm](https://safedep.io/redhat-cloud-services-hit-by-mini-shai-hulud-npm-worm/), 1 June 2026.
-   OSV, [MAL-2026-5145: malicious code in @redhat-cloud-services/patch-client](https://osv.dev/vulnerability/MAL-2026-5145), from GitHub advisory [GHSA-vx4h-8456-6pp3](https://github.com/advisories/GHSA-vx4h-8456-6pp3).
-   StepSecurity, [binding.gyp npm supply chain attack spreads like a worm](https://www.stepsecurity.io/blog/binding-gyp-npm-supply-chain-attack-spreads-like-worm), 3 June 2026.
-   JFrog Security Research, [Miasma worm returns to npm](https://research.jfrog.com/post/miasma-worm-returns-to-npm/), 14 July 2026.
-   OSV, [MAL-2025-190636: malicious code in @asyncapi/generator](https://osv.dev/vulnerability/MAL-2025-190636), covering 3.3.1.

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.

[Subscribe to teardowns](https://chain305.com/teardowns/) [Read what a package firewall catches →](https://chain305.com/what-is-a-package-firewall/)
