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
nextbranch injected a dropper into three packages, triggeringrelease-with-changesets.ymlto 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
masterbranch injected a dropper intoindex.js, triggeringif-nodejs-release.ymlto publish@asyncapi/specs@6.11.2-alpha.1and@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.
Affected Packages
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:
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.
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.comdht.transmissionbt.com
Link to the run : https://app.stepsecurity.io/github/actions-security-demo/comp-packages/actions/runs/29320038189?tab=network-events

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.1packages/helpers/src/utils.js→ ships in@asyncapi/generator-helpers@1.1.1packages/components/src/utils/ErrorHandling.js→ ships in@asyncapi/generator-components@0.7.1(present in the babel-compiledlib/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.
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:
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:
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). Achannel-orchestrator.jscoordinates 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 inrecon/token-vault.jstargetsGITHUB_TOKEN,NPM_TOKEN, andPYPI_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 setspropagate.npm: falsefor 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.
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.




