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.

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:

WaveWhenPackagesCode ran from
Red Hat1 June 202632 @redhat-cloud-services packages, 96 versionsA new preinstall hook
Phantom Gyp3 June 2026, from 23:30 UTC57 packages, 286+ versions, starting with @vapi-ai/server-sdkA 157-byte binding.gyp
AsyncAPI14 July 2026@asyncapi/generator, generator-helpers, generator-components and specs, 5 versionsModule-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

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.