We've identified five trojanized @asyncapi packages published on 2026-07-14. The attacker stole an npm publish token by exploiting a pull_request_target workflow vulnerability in the AsyncAPI generator repository, then injected an obfuscated downloader into normal runtime modules across four packages. Importing any of the affected packages fetches an encrypted Node.js loader from IPFS, writes it to disk as sync.js , and starts it as a detached process.
The chain ends in a persistent implant with a real remote shell. The payload framework self-identifies as M-RED-TEAM v6.4 in its own code . Credential harvesting and self-propagation are present in the code but disabled in this build. The shell is sufficient for the operator to collect data and run arbitrary commands without those features.
The packages combined see roughly 2.9 million weekly downloads, with @asyncapi/specs alone accounting for around 2.7 million.
The asyncapi/generator repository used a GitHub Actions workflow with a pull_request_target trigger. This trigger runs with access to repository secrets even when the workflow checks out code from an external pull request, a well-known footgun.
A contributor identified the vulnerability and opened a fix PR ( #2092 ) on May 17. It was still unmerged nearly 2 months later when the attack happened.
At 05:08 UTC on July 14, the attacker opened 37 pull requests against the generator repository. One of them, PR #2155 , contained obfuscated JavaScript that exfiltrated the npm publish token to rentry[.]co . The workflow completed at 05:16 UTC. With the token in hand, the attacker pushed malicious commits to the branch at 06:58 UTC and published the first compromised packages at 07:10 UTC. They then pivoted to asyncapi/spec-json-schemas , pushing 11 commits between 07:51 and 08:28 UTC to publish the specs versions.
The attack involves three overlapping signals that don't all point to the same place.
The initial access technique (a PR flood targeting a pull_request_target workflow with a rentry[.]co dead-drop) matches patterns from the prt-scan campaign previously observed in similar GitHub Actions secret-theft attacks.
The payload framework self-identifies as M-RED-TEAM v6.4 in code throughout the recovered stage-3 source. That is the most direct label the code gives itself.
The artifact names and configuration use Miasma branding: the build target is miasma-train-p1 , the runtime lock and identity paths sit under ~/.config/.miasma/ , persistence artifacts are named miasma-monitor , and the spawn certificates use the format string miasma-spawn-cert-v1 . These overlap with the earlier Miasma toolkit , though a SafeDep researcher noted the payloads differ : the prior build was Bun-based with RSA/AES-CBC, active propagation, and a destructive deadman; this one is Node-based with secp256k1/AES-GCM, HTTP C2, and those features switched off.
We can't determine from the evidence whether the prt-scan initial access and M-RED-TEAM payload represent a single operator or separate parties. The Miasma branding may reflect code reuse, imitation, or deliberate mislabeling. No definitive attribution is made here.
The compromised releases:
The malicious code is not in an npm lifecycle hook. It was placed in modules that run during normal use: the specs entry point, a generator validator, a helper utility, and a components error-handling module. The payload runs when the module loads, so a plain require() is enough to trigger it.
In @asyncapi/specs , the downloader is prepended to the real schema exports:
The downloader runs in a detached child process. After calling child.unref() , the parent exits immediately and the download continues in the background.
The node -e payload is obfuscated, but its string lookup table contains the IPFS URL and drop filename in plaintext:
The specs branch fetches CID Qmet4fhsAaWMBUxNDfREHwgiyDeSWy4YSYs9wiKUW5jGyf . The generator-family branch fetches QmQobZSp1wRPrpSEQ56qnyq7ecZh5Bg5k1fnjt4SUwwHb9 . Both write sync.js to a per-user NodeJS data directory: ~/Library/Application Support/NodeJS on macOS, %LOCALAPPDATA%\NodeJS on Windows, ~/.local/ /NodeJS on Linux.
The two IPFS objects are obfuscated JavaScript loaders: 8,243,380 bytes (specs) and 8,254,481 bytes (generator family). Each derives an AES-256-GCM key via HKDF-SHA256, decrypts an embedded vault, reverses a printable-ASCII rotation, and evaluates the result. We extracted the logic into a non-executing decryptor:
The GCM authentication tags validate for both builds. Each loader also contains a sourceBundle field encrypted with the same key; it matches the recovered stage-3 file byte for byte. The baked config uses a separate key derived from rt-baked-key and the same hard-coded master.
The two recovered stage-3 files:
The generator-family build adds one behavioral difference: a timer that re-checks the primary C2 after failover and switches back when it recovers. Other differences are generated junk declarations.
Both builds include a two-certificate secp256k1 spawn chain. Both signatures verify. The chain does not block this seed from running.
Early reporting characterized this as a "safe canary" based on config field values. The baked config we recovered:
None of the three field values hold up under call-graph analysis:
On first run the payload generates a secp256k1 keypair and stores it in a platform-specific path disguised as a system cache file. It uses ~/.config/.miasma/run/node.lock to prevent duplicate instances.
Persistence by platform:
The implant beacons to hxxp://85[.]137[.]53[.]71:8080 roughly every 30 seconds. Beacons are signed and encrypted with the attacker's public key. Even with recon disabled, each beacon includes redacted previews of PATH , , USER , and HOSTNAME , and checks for the presence of Cursor, Claude, and VS Code config files under /app .
Commands are normally delivered in an encrypted envelope. When no encrypted bundle is present the handler falls back to a plaintext commands array:
Because the C2 is HTTP, an on-path attacker can inject commands via this path.
Command 11 ( ShellExec ) passes the request to child_process.exec() . The only blacklisted command is killall :
File commands (list, get, put) are sandboxed to a hard-coded /sim-fs path. The delete command is not implemented. The shell has no such restriction.
Payload updates come from two sources: an operator command supplying a new CID, or a two-minute polling loop checking Nostr and Ethereum for a higher version. Nostr updates are signature-verified. Ethereum records are accepted without signature verification ( trustUnsignedBlockchain: true ). A fetched update is written to disk as a randomized .bin / .cache / .dat file, launched with Node, and allowed to replace the current process.
The bundle contains a full credential harvester covering cloud credentials, package manager tokens, SSH/GPG keys, shell histories, browsers, CI/CD systems, AI developer tools, databases, wallets, containers, and password managers. It can download Gitleaks and HackBrowserData to assist collection. None of this runs because toggles.recon is false; the harvester exits before collecting anything. The shell can achieve the same result manually.
Propagation vectors for npm, PyPI, and Cargo are present and implemented. All propagation toggles are false, the only trySpread() call is guarded by persistent mode (which returns before reaching it), and the Propagate command is unimplemented. No spread occurs.
The mutation engine, evasion checks, AI-tool poisoning, and deadman switch are all toggled off. The wipe implementation writes a marker file to ~/Documents/SIMULATION_WIPE_TRIGGERED.txt rather than deleting anything.
HTTP on port 8080 is the only real beacon and command channel. The other protocols have narrower roles:
Several generic upload and command methods on the lower-tier transports are no-ops in this build.
PackageVersionSHA-256 @asyncapi/specs6.11.29b2e65db653ca8575c9b10eefb9a80c6006404812c2ec212bf5675e3c690233b@asyncapi/specs6.11.2-alpha.1d425e4583cc6185d41e95c45eda00550045a5d1919b9a012236a4520d009dbd7@asyncapi/generator3.3.1bfaeb987faa6de2b5a5eb63b1233d055215b09b0349a9394f2175fd7cdf385e4@asyncapi/generator-helpers1.1.134014776d3d3ff11bc4439b02fd7ac0f02a887eb3a052eeafff236e2f6db8ad1@asyncapi/generator-components0.7.1082d733db0687dcd768104972b065d4b58cb1e6043688c6c20fa3702337f36ab
Downgrade to @asyncapi/[email protected] , @asyncapi/[email protected] , @asyncapi/[email protected] , and @asyncapi/[email protected] . Remove the five compromised versions from manifests, lockfiles, caches, internal mirrors, and build images. Hunt for systems that imported the affected modules, not just systems where the package was installed, since the implant runs on require() .
On any suspected host: isolate and preserve volatile state first. for the drop, lock, identity, and persistence paths listed above, and for unusual detached Node processes. Check for connections to the C2 ports and for Node activity correlated with IPFS, Nostr, Ethereum RPC, DHT, or mDNS.
Treat credentials available to an affected developer machine or build host as potentially exposed via shell commands. Rotate npm tokens, source control access, cloud credentials, CI/CD secrets, SSH keys, signing keys, and browser sessions from a clean machine. Rebuild compromised hosts.
The @asyncapi/[email protected] tarball is still downloadable at its direct URL despite being absent from registry metadata. It needs to be purged from backing storage and CDN.
If you are an Aikido user, check your central feed and filter on malware issues. All five compromised releases surface as a 100/100 critical issue. If you don't have an account yet, create one and connect your repos — malware coverage is included in the free plan, no credit card required.
Aikido Device Protection gives you visibility across the packages installed on your team's devices, including libraries, IDE plugins, and build dependencies. Aikido Safe Chain (open source) sits in your existing workflow and checks packages against Aikido Intel before npm, yarn, or pnpm installs them.
Secure your code, cloud, and runtime in one central system. Find and fix vulnerabilities fast automatically.
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
