Back to Blog

Coordinated AsyncAPI Supply Chain Attack: Miasma RAT Delivered via Compromised CI/CD Pipelines in Two Repositories

On July 14, 2026 at 07:10 UTC, three packages in the AsyncAPI generator monorepo (@asyncapi/generator@3.3.1, @asyncapi/generator-helpers@1.1.1, and @asyncapi/generator-components@0.7.1) were published to npm carrying an obfuscated dropper that fires the moment the library is loaded, not on install. The packages were published through the project's own legitimate GitHub Actions release workflow and carry valid npm OIDC provenance attestations, because the attacker didn't steal an npm token: they gained push access to the repository's next branch and let the project's real CI/CD pipeline do the publishing for them.
Rohan Prabhu
View LinkedIn

July 14, 2026

Share on X
Share on X
Share on LinkedIn
Share on Facebook
Follow our RSS feed
Table of Contents

On July 14, 2026, a coordinated supply chain attack hit two separate AsyncAPI GitHub repositories and produced four malicious npm packages carrying an obfuscated dropper that fires when the library is loaded, not at install time. The attacker gained push access to both repositories and, in each case, used the project's own legitimate GitHub Actions release pipeline to publish packages with valid OIDC provenance attestations. No npm token was stolen.

  • asyncapi/generator (06:58 UTC): commit pushed to the next branch injected a dropper into three packages, triggering release-with-changesets.yml to publish @asyncapi/generator@3.3.1, @asyncapi/generator-helpers@1.1.1, and @asyncapi/generator-components@0.7.1.
  • asyncapi/spec-json-schemas (07:56 UTC): a series of commits pushed to the master branch injected a dropper into index.js, triggering if-nodejs-release.yml to publish @asyncapi/specs@6.11.2-alpha.1 and @asyncapi/specs@6.11.2.

Both attacks share the same placeholder git identity ("Your Name" <you@example.com>, GitHub login invalid-email-address), the same Miasma RAT second-stage payload infrastructure, and the same drop behavior. We independently downloaded all tarballs and statically decoded the payload without executing any of it.

Both attacks are CI/CD pipeline compromises, not stolen npm tokens or malicious maintainers. The attacker pushed commits under a placeholder git identity and let each repository's real release workflow do the publishing via npm's GitHub OIDC trusted-publisher integration. The resulting packages carry legitimate SLSA provenance attestations, proving only that the project's authorized workflow produced them, not that the triggering commits were legitimate. Provenance does not protect against a compromised push credential.

Current status (as of 11:18 UTC, July 14, 2026): All five malicious versions have been unpublished from the npm registry. The latest dist-tag for each package now resolves to a clean version: @asyncapi/generator@3.3.0, @asyncapi/generator-helpers@1.1.0, @asyncapi/generator-components@1.0.0, and @asyncapi/specs@6.11.1. Fresh installs are safe. Existing installations and lock files from the exposure window are not.

Harden-Runner Blocked Exfiltration During the AsyncAPI Supply Chain Attack

The StepSecurity threat intelligence team identified the compromised packages within the first 30 minutes of publication. To protect pipelines, we needed to know where the malware sends stolen data.

To find out, the team ran the compromised packages in an isolated GitHub Actions job monitored by Harden-Runner. The malware executed, and its network activity appeared directly in Harden-Runner's telemetry: the HTTP C2 server, plus fallback channels over IPFS gateways, Nostr relays, and BitTorrent DHT.

The team added the C2 endpoints to Harden-Runner's Global Block List, a centrally maintained list of domains and IP addresses tied to active supply chain attacks. Updates to the list take effect immediately across every workflow using Harden-Runner. No configuration change, no version bump, no workflow edit required.

Once the IOCs were on the list, any pipeline that installed the compromised packages had its connections to the attacker's infrastructure blocked at the network layer.

Affected Packages

Package Malicious Version Last Safe Version
@asyncapi/generator 3.3.1 3.3.0
@asyncapi/generator-helpers 1.1.1 1.1.0
@asyncapi/generator-components 0.7.1 0.7.0
@asyncapi/specs 6.11.2 6.11.1
@asyncapi/specs 6.11.2-alpha.1 6.11.1

Background:

@asyncapi/generator is the official code/template generator for the AsyncAPI specification, used to scaffold client and server code, documentation, and HTML from AsyncAPI documents. generator-helpers and generator-components are supporting packages from the same monorepo. @asyncapi/specs (published from the separate asyncapi/spec-json-schemas repo) bundles the canonical JSON Schema definitions for the AsyncAPI specification and is consumed as a direct dependency of many AsyncAPI tooling packages including the parser and generator. All four packages see substantial download volume across API tooling, CI pipelines, and documentation workflows.

Attack 1: asyncapi/generator, next Branch (06:58 UTC)

The malicious commit, 3eab3ec9304aa26081358330491d3cfeb55cc245, was pushed directly to next at 06:58:42 UTC. The commit's author and committer identity is the unconfigured git default ("Your Name" <you@example.com>), and the commit is unsigned. GitHub could not map the pushing account to a real user (it resolves to the generic invalid-email-address placeholder), consistent with a compromised credential or leaked automation token rather than a legitimate contributor.

Twelve seconds after the push landed, the repository's release-with-changesets.yml workflow ran (Actions run 29313420558) on refs/heads/next and published all three packages via npm's GitHub Actions OIDC trusted-publisher integration at 07:10 UTC. We confirmed this directly from each package's npm provenance attestation: the embedded sigstore Fulcio certificate's Subject Alternative Name records repo:asyncapi/generator:ref:refs/heads/next, the exact workflow file, the commit SHA, and the run URL, identical across all three packages, confirming a single coordinated publish from one compromised push.

Attack 2: asyncapi/spec-json-schemas, master Branch (07:51 UTC)

Separately, the same attacker hit the asyncapi/spec-json-schemas repository using an identical push identity: "Your Name" <you@example.com> / GitHub login invalid-email-address. This time the target was the master branch, and the release trigger was different: the repository's if-nodejs-release.yml workflow fires on any push to master whose commit message starts with fix: or feat:.

The attacker pushed a series of commits between 07:51 and 08:28 UTC. The key events:

Commit Time (UTC) Message What it did
36269ce81837 07:56:11 "fix: correct JSON schema" Injected obfuscated dropper into index.js, padded with leading spaces to push it off-screen in the diff view.
49cb17a9f920 07:56:14 "6.13.5" Version bump; triggered a workflow run, but not the fix:/feat: prefix path to publish.
61a930fca724 08:04:02 "fix: correct JSON schema" Second payload commit; published @asyncapi/specs@6.11.2-alpha.1 at 08:06 UTC.
689f5b96693a 08:28:02 "fix: patch parser version" Added one blank line to package.json; triggered the final workflow run that published @asyncapi/specs@6.11.2 at 08:30 UTC.

The npm provenance attestation for @asyncapi/specs@6.11.2 records workflow .github/workflows/if-nodejs-release.yml, branch refs/heads/master, and commit 689f5b96693ab1f82a825b6d7c4ee566b0afc4c6, confirming the publish originated from the attacker's final commit. The master branch HEAD remains at the attacker's commit at the time of this writing.

The dropper in the specs tarball differs from the generator attack in two ways: it is an ESM/TypeScript-compiled build output (50-line index.js) rather than raw CommonJS, and the inline stage-2 payload uses a different obfuscator build (decoder functions _0x285e/_0x3c84, rotation target 0xf214d). Functionally, however, it is identical: fetch sync.js from IPFS, drop to the same OS-specific hidden "NodeJS" directory, spawn detached and hidden, exit cleanly. The IPFS CID is different from the generator attack (Qmet4fhsAaWMBUxNDfREHwgiyDeSWy4YSYs9wiKUW5jGyf), but the drop paths, C2 infrastructure, and Miasma RAT payload are the same.

Is your release workflow one compromised push away from publishing for an attacker? Run any GitHub Action through the free GitHub Actions Advisor to check its security posture in seconds. No signup required.

Harden Runner Analysis

We ran the payload chain in a monitored, isolated GitHub Actions job wrapped with Harden-Runner to capture the actual runtime network behavior of the second-stage sync.js process, independent of any third-party reporting. Harden-Runner's egress monitoring recorded outbound connection attempts to:

  • 85.137.53.71 - the reported HTTP C2 endpoint. Harden-Runner observed the connection attempt directly, so we now consider this IP StepSecurity-confirmed rather than third-party-reported.
  • router.bittorrent.com
  • dht.transmissionbt.com

Link to the run : https://app.stepsecurity.io/github/actions-security-demo/comp-packages/actions/runs/29320038189?tab=network-events

Want this level of visibility into your own CI/CD pipelines? Get a free GitHub Actions security audit of your organization's workflows, including egress baselines and risky permissions.

Payload Capabilities

The commit injected the same ~7.7KB obfuscator.io-obfuscated payload, padded onto a single line with roughly a thousand leading spaces to push it off-screen in a diff view, into three files, one per affected package:

  • apps/generator/lib/templates/config/validator.js → ships in @asyncapi/generator@3.3.1
  • packages/helpers/src/utils.js → ships in @asyncapi/generator-helpers@1.1.1
  • packages/components/src/utils/ErrorHandling.js → ships in @asyncapi/generator-components@0.7.1 (present in the babel-compiled lib/ output of the published tarball)

There is no preinstall/postinstall/install script anywhere in any of the three package.json files. This dropper fires when the poisoned module is require()d during normal use of the generator: the moment a build or CI job actually calls into the library, not at npm install time.

We decoded both obfuscation layers ourselves by reimplementing the obfuscator.io string-array-and-rotation cipher in Python. No code from the package was ever executed.

Attack Flow Diagram

ATTACKER Compromised push credential · same placeholder identity used in both repositories "Your Name" <you@example.com> · GitHub login: invalid-email-address asyncapi/generator Branch: next · Commit 3eab3ec9 · 06:58 UTC Dropper in validator.js · utils.js · ErrorHandling.js 3 npm packages affected asyncapi/spec-json-schemas Branch: master · Commit 36269ce8 · 07:56 UTC Dropper injected into ESM-compiled index.js 1 npm package affected · 58 min after first attack release-with-changesets.yml Triggers on push to next branch GitHub OIDC · valid SLSA provenance · 07:10 UTC if-nodejs-release.yml Triggers on fix: / feat: prefix commit to master GitHub OIDC · valid SLSA provenance · 08:06–08:30 UTC npm Registry — 4 Malicious Packages with Valid SLSA Provenance @asyncapi/generator@3.3.1 @asyncapi/generator-helpers@1.1.1 @asyncapi/generator-components@0.7.1 @asyncapi/specs@6.11.2 ⚠ Transitive: @asyncapi/parser specifies @asyncapi/specs: ^6.11.1 — semver range includes malicious 6.11.2 Victim Machine — 3-Stage Dropper Stage 1 Fires on require() not on npm install spawn(node, ['-e', stage2]) {detached, windowsHide} Stage 2 Download sync.js from IPFS QmQobZSp1... (generator pkgs) Qmet4fhsA... (specs pkg) Drop to ~/NodeJS/sync.js Stage 3 — Miasma RAT v3 3.08MB bundled Node.js app AES-256-GCM + HKDF-SHA256 campaign: miasma-train-p1 safeMode: true · propagate: false Drop paths: Linux ~/.local/share/NodeJS/ · macOS ~/Library/Application Support/NodeJS/ · Windows %LOCALAPPDATA%\NodeJS\ Capabilities: credential harvesting (130+ types) · AI tool poisoning · LAN lateral movement · 6 C2 channels · persistence AI poisoning targets: Claude Code · GitHub Copilot · Cursor · metamorphic mutation · libp2p GossipSub mesh propagation Attacker root EC key 0432fa4b... signs bot sub-keys · LAN subnet scan · mDNS discovery · 130+ credential file targets HTTP C2 85.137.53.71 :8080 commands · :8081 upload :8091 proxy management Not seen in prior Miasma waves Nostr Relay C2 wss://relay.damus.io wss://relay.nostr.com/ Decentralised · censorship-resistant New capability vs prior Miasma waves BitTorrent DHT + Blockchain router.bittorrent.com:6881 dht.transmissionbt.com:6881 + Ethereum 0x12c37A86a0Ed0beBe5d... New capability vs prior Miasma waves Credential Exfiltration + Persistence npm · GitHub · PYPI · SSH keys · AWS · Azure · GCP · browser passwords · macOS Keychain · kube/config · Docker Persistence: systemd · crontab · launchd · Windows Registry Run keys · LAN lateral movement via subnet scan and mDNS

Stage 1: fires on require()

spawn("node", ["-e", <stage2>], {
  detached: true,
  stdio: "ignore",
  windowsHide: true
}).unref()

Stage 2: IPFS-hosted downloader

The decoded stage-1 argument creates a hidden, OS-specific directory disguised as a Node.js runtime folder, fetches a file from a public IPFS gateway, and launches it the same way (detached, output-suppressed, window-hidden) before calling process.exit(0) so the original node process running the generator exits normally with no visible error:

OS Drop path
Linux ~/.local/share/NodeJS/sync.js
macOS ~/Library/Application Support/NodeJS/sync.js
Windows %LOCALAPPDATA%\NodeJS\sync.js

Source URL: https://ipfs.io/ipfs/QmQobZSp1wRPrpSEQ56qnyq7ecZh5Bg5k1fnjt4SUwwHb9

Stage 3: sync.js

We reconstructed the AES-256-GCM decryption pipeline from static analysis of the file's structure: HKDF-SHA256 with key material rt-file-key-material-v1 and info string rt-file-key produces the AES key; a ROT cipher (shift=4, delta=90, printable ASCII range 33-126) decodes the intermediate form to executable JS. The decrypted payload is a 3.08MB bundled Node.js application, self-identified as "Miasma v3."

Baked configuration

sync.js contains an AES-256-GCM-encrypted "baked config" blob, keyed with HKDF(sha256, rt-vault-master-key-32b-aaaaaaaa, "", rt-baked-key, 32). After decryption, the config reveals the full C2 infrastructure baked into this specific deployment:

Field Value Role
c2Server http://85.137.53.71:8080 Primary C2 (commands)
uploadServer http://85.137.53.71:8081 Credential exfiltration upload
c2ProxyMgmt http://85.137.53.71:8091 C2 proxy management
nostrRelays wss://relay.damus.io, wss://relay.nostr.com/ Decentralized C2 via Nostr protocol
blockchain.contractAddress 0x12c37A86a0Ed0beBe5d1d6a43E42f07860eAc710 Ethereum mainnet C2 dead-drop
dhtBootstrap router.bittorrent.com:6881, dht.transmissionbt.com:6881 BitTorrent DHT rendezvous
target.name miasma-train-p1 Campaign identifier (self-attributed)
maxGen 4 Maximum worm propagation generations
batch.defaultStrategy CANARY Deploy to 5% of targets first, then waves of 100

RAT architecture

The decrypted 3.08MB payload bundles a complete framework under the namespace packages/core/. The source paths embedded in the bundle reveal a modular architecture designed for long-term persistent access:

  • Six independent C2 channels, each with its own module: HTTP REST (c2-rest-channel.js), Nostr relay (nostr-channel.js), IPFS (ipfs-channel.js), BitTorrent DHT (dht-channel.js), libp2p GossipSub P2P mesh (libp2p-channel.js, gossip-transport.js), and Ethereum blockchain (blockchain-channel.js). A channel-orchestrator.js coordinates failover between them. The multi-channel design means blocking any single C2 endpoint does not sever the connection.
  • Credential harvesting (recon/credential-harvester.js): targets confirmed in source code include browser Login Data, Cookies and Local State files (Chrome, Brave, Edge profiles); SSH keys (~/.ssh/id_rsa, id_ed25519); ~/.npmrc, ~/.gitconfig, GitHub CLI config (~/.config/gh); AWS credentials (~/.aws/credentials); kube/config; Docker credentials; and macOS Keychain. Token-specific handling in recon/token-vault.js targets GITHUB_TOKEN, NPM_TOKEN, and PYPI_TOKEN.
  • AI tool poisoning (recon/ai-tool-poisoner.js): the presence of this module indicates capability to inject malicious context or instructions into AI coding assistant sessions (such as Claude Code, GitHub Copilot, or Cursor). If this module is active on a developer workstation, AI-assisted code or commands from that session should be treated as potentially compromised.
  • LAN lateral movement (comm/subnet-scan.js, comm/lan-discovery.js, comm/mdns-channel.js): the payload can enumerate and attempt to spread across the local network, not just the initial host.
  • Metamorphic engine (metamorphic/mutation-engine.js): each propagated copy is re-obfuscated by a mutation engine. The decrypted payload header reads // mutated v3 profile=low runtime=1, confirming this is generation 3 of a polymorphic lineage.
  • Worm propagation (propagate/): five propagation modules handle build-toolchain injection, runtime injection into running processes, and vector selection. The baked config sets propagate.npm: false for this deployment, suggesting the attacker chose targeted delivery over mass npm worm spread in this campaign wave.
  • Persistence (deadman/real-persistence-writer.js): writes persistence entries using systemd, crontab, macOS launchd, and Windows Registry autostart keys.
  • Deadman/self-destruct (deadman/safe-wipe.js): capability to securely erase evidence and terminate the implant on command or triggered condition.
  • Shell execution (comm/shell-executor.js): arbitrary command execution capability, controlled from C2.

sync.js establishes a two-entry EC public key chain (__RT_SPAWN_CHAIN_B64__) rooted at attacker public key 0432fa4b.... This chain signs bot instances with delegated sub-keys, so the C2 operator can authenticate and command individual bots cryptographically without revealing the root key. This is consistent with a professionally built botnet framework, not a one-off credential stealer.

We'll continue to update the blog post as new findings emerge and our understanding evolves.

Remediation

1 . Regenerate your lock file. All malicious versions have been unpublished; npm install will no longer resolve them. However, any existing lock file generated between 07:10 and 11:18 UTC on 2026-07-14 may still pin the malicious versions. Delete and regenerate:

rm package-lock.json   # or yarn.lock / pnpm-lock.yaml
npm install

If you need to pin explicitly:

npm install @asyncapi/generator@3.3.0 @asyncapi/generator-helpers@1.1.0 @asyncapi/generator-components@1.0.0
npm install @asyncapi/specs@6.11.1

For projects that received @asyncapi/specs@6.11.2 only as a transitive dependency via @asyncapi/parser, use overrides to force the safe version:

// package.json
"overrides": {
  "@asyncapi/specs": "6.11.1"
}

2 . Check for and remove the drop file at the OS-appropriate path listed above, and kill any orphaned node processes launched from a "NodeJS" directory under your home folder.

3 . Rotate all credentials that were present on any affected machine or CI runner: npm tokens, GitHub PATs/deploy keys, SSH private keys, AWS credentials (~/.aws/credentials), macOS Keychain entries, and browser-saved passwords and cookies (Chrome, Brave, Firefox, Edge profiles all targeted, confirmed via static analysis).

4 . Treat AI coding assistant sessions as potentially compromised on any affected developer workstation. The payload includes an ai-tool-poisoner.js module; any AI-assisted code written on that machine during the exposure window should be reviewed independently.

5 . Audit build and CI logs for the affected window for unexpected outbound network activity, especially to ipfs.io, 85.137.53.71 (any port), the BitTorrent DHT bootstrap nodes, or the Nostr relay domains.

Attack Flow Timeline

06:58 UTC — ATTACK BEGINS
Attacker pushes 3eab3ec9 to asyncapi/generator next branch under placeholder identity “Your Name” <you@example.com>. Dropper injected into three source files.
07:10 UTC — MALICIOUS PACKAGES PUBLISHED
release-with-changesets.yml triggers via GitHub OIDC. Publishes three malicious packages with valid SLSA provenance:
  • @asyncapi/generator@3.3.1
  • @asyncapi/generator-helpers@1.1.1
  • @asyncapi/generator-components@0.7.1
07:51–07:56 UTC — SECOND REPO COMPROMISED
Attacker begins pushing to asyncapi/spec-json-schemas master branch. Commit 36269ce8 at 07:56 injects ESM-compiled dropper into index.js — 15 minutes after public disclosure of the generator attack.
08:06 UTC — SPECS ALPHA PUBLISHED
if-nodejs-release.yml fires on fix: prefix commit. Publishes @asyncapi/specs@6.11.2-alpha.1.
08:30 UTC — MALICIOUS SPECS PUBLISHED
Attacker’s final commit 689f5b96 triggers publish of @asyncapi/specs@6.11.2. Package receives 2.66M downloads/week and is pulled transitively by @asyncapi/parser (970K/week) via ^6.11.1 semver range.
REMEDIATION
11:12 UTC — GENERATOR REMEDIATED
@asyncapi/generator@3.3.1 unpublished. latest reverted to 3.3.0. Exposure window: 4h 2m.
11:13 UTC — HELPERS REMEDIATED
@asyncapi/generator-helpers@1.1.1 unpublished. latest reverted to 1.1.0. Exposure window: 4h 2m.
11:14 UTC — COMPONENTS REMEDIATED
@asyncapi/generator-components@0.7.1 unpublished. latest set to pre-existing clean version 1.0.0. Exposure window: 4h 3m.
11:18 UTC — SPECS REMEDIATED
@asyncapi/specs@6.11.2 and 6.11.2-alpha.1 unpublished. latest reverted to 6.11.1. Exposure window: 2h 48m (6.11.2), 3h 12m (6.11.2-alpha.1). All five malicious versions now gone from npm.

For StepSecurity Customers

Harden Runner

Harden-Runner would have flagged this dropper at multiple points in the chain: the outbound network connection from a GitHub Actions runner to ipfs.io during a build step that only expects registry traffic, the detached child process spawned with hidden/ignored I/O, the write of an executable script to a non-standard hidden directory, and the subsequent DHT and C2 connections documented above. Harden-Runner's egress control would block the connection to the known C2 IP 85.137.53.71 outright. And if the workflow had Harden-Runner running in block mode, any of these anomalous network calls, including the unexpected connections to ipfs.io and the BitTorrent DHT bootstrap nodes, would have been blocked at the point they deviated from the runner's established network baseline, stopping the payload before it could exfiltrate anything.

Secure Registry

StepSecurity Secure Registry provides each enterprise customer with a dedicated, policy-enforced npm registry that sits between your existing package manager (such as JFrog Artifactory) and the public npm registry. Instead of fetching packages directly from registry.npmjs.org, your infrastructure routes requests through your StepSecurity registry, which applies configurable security policies before serving any package.

The primary defense here is the cooldown period. Newly published package versions are held for a configurable window before being served to any developer machine or CI/CD pipeline. When the compromised Miasma packages were published to npm, including @vapi-ai/server-sdk, ai-sdk-ollama, and dozens of packages in the jagreehal ecosystem, Secure Registry customers were never exposed.

Detect Compromised Developer Machines

StepSecurity Dev Machine Guard gives security teams real-time visibility into npm packages installed across every enrolled developer device. When a malicious package is identified, teams can immediately search by package name and version to discover all impacted machines.

npm Package Cooldown Check

Newly published npm packages are temporarily blocked during a configurable cooldown window. When a PR introduces or updates to a recently published version, the check automatically fails. Since most malicious packages are identified within hours, this creates a crucial safety buffer. In this case, 57 packages across 286+ malicious versions were published in a rolling campaign lasting under two hours on June 3, so any PR updating to an affected version during the cooldown period would have been blocked automatically.

npm Package Compromised Updates Check

StepSecurity maintains a real-time database of known malicious and high-risk npm packages, updated continuously, often before official CVEs are filed. If a PR attempts to introduce a compromised package, the check fails and the merge is blocked. All compromised versions from this Miasma campaign, including @vapi-ai/server-sdk, ai-sdk-ollama, and the full jagreehal package family, were added to this database within minutes of detection.

npm Package Search

Search across all PRs in all repositories across your organization to find where a specific package was introduced. When a compromised package is discovered, instantly understand the blast radius: which repos, which PRs, and which teams are affected. This works across pull requests, default branches, and dev machines.

Secure your CI/CD pipeline with StepSecurity
Start scanning and hardening your GitHub Actions workflows today. Free to get started, no credit card required.
Get started for free

Explore Related Posts