A Misconfigured GitHub Action Backdoored Packages With 3M Weekly Downloads
A pull_request_target flaw in AsyncAPI's CI let attackers steal a token and trojan 4 npm packages — 3M+ weekly downloads — with credential-stealing code.
On this page
Somewhere in AsyncAPI’s pull request queue, a fix for a CI workflow bug had been sitting since mid-May — reviewed, technically sound, and stuck. Fifty-eight days later, on July 14, 2026, someone stopped waiting for a maintainer to merge it and used the exact hole it would have closed instead. In just over three hours, five poisoned package versions were live on npm’s public registry, and every one of the 3 million-plus weekly installs of AsyncAPI’s tooling became a potential vector for a credential-harvesting framework that identifies itself, almost tauntingly, as M-RED-TEAM v6.4.
What exactly did the attacker exploit?
A pull_request_target misconfiguration in a single workflow file: manual-netlify-preview.yml. That trigger is designed for jobs that need access to a repository’s secrets — posting a preview URL as a PR comment, for instance — while still running against pull requests from forks. The catch is that pull_request_target runs with the base repository’s permissions and secret store, so if the workflow also checks out and executes the pull request’s own head code, an attacker gets their code running inside a privileged context. Wiz’s writeup has a name for that exact shape: a “pwn request.”
AsyncAPI’s workflow reproduced it. The attacker opened PR #2155 at 05:08 UTC with an obfuscated payload buried after roughly 1,000 bytes of whitespace, according to Wiz. The workflow run completed at 05:16 UTC, and the stolen credentials were exfiltrated to a paste on rentry.co. From there, the attacker used the privileged token to push a malicious commit to AsyncAPI’s next branch at 06:58 UTC and, per Chainguard, trigger the project’s own release pipeline directly — no further social engineering required.
Wiz doesn’t describe this as novel tradecraft — it calls it a textbook “pwn request”: a workflow triggered by pull_request_target that runs with the base repository’s secrets while still checking out the untrusted PR head. AsyncAPI’s manual-netlify-preview.yml reproduced that exact shape, and a contributor had already flagged it before an attacker found it.
How did the compromise unfold, minute by minute?
Fast, once the token was in hand. From PR to five published packages, the entire operation ran inside a single morning:
| Package | Malicious version | Published (UTC) |
|---|---|---|
| @asyncapi/generator-helpers | 1.1.1 | 07:10:42 |
| @asyncapi/generator-components | 0.7.1 | 07:10:44 |
| @asyncapi/generator | 3.3.1 | 07:10:48 |
| @asyncapi/specs | 6.11.2-alpha.1 | 08:06:20 |
| @asyncapi/specs | 6.11.2 | 08:30:09 |
The contrast between how long the defense sat idle and how fast the offense moved once it started is the story in miniature.
A contributor identified the exact pull_request_target misconfiguration and submitted a remediation. Per Wiz and Chainguard, it remained unmerged for 58 days. Microsoft’s own timeline traces the underlying proof-of-concept even earlier, to April 29 — visibility existed for nearly three months before anyone exploited it.
From PR #2155 landing at 05:08 UTC to the last trojanized @asyncapi/specs release at 08:30:09 UTC, the entire compromise — token theft, credential exfiltration to rentry.co, a malicious commit, and five package publishes — took roughly three hours and twenty minutes.
What did the payload actually do once it landed on a machine?
It ran when a developer’s code imported the package — not when npm installed it. Microsoft’s analysis is explicit on this point: the malware executed at module-load (import/require) time, which means the standard defense of running npm install --ignore-scripts did nothing, because there was no install script to skip. The malicious logic lived inside the package’s runtime code itself.
Wiz traces a three-stage infection chain. Stage one spawned a child process that downloaded an 8.25 MB encrypted bundle over IPFS to an OS-specific path — ~/.local/share/NodeJS/sync.js on Linux, ~/Library/Application Support/NodeJS/sync.js on macOS, %LOCALAPPDATA%\NodeJS\sync.js on Windows. Stage two unpacked that bundle’s configuration and runtime. Stage three deployed a 92,000-line, modular malware framework that established persistence via a systemd service — Chainguard names it miasma-monitor.service on Linux — and maintained command-and-control through a mix of HTTP, Nostr messaging, Ethereum smart contracts, and peer-to-peer networks. Both Wiz and Chainguard describe it as a remote-access toolkit rather than a self-propagating worm.
The target list reads like a credential inventory of a working developer’s entire machine: browser-saved passwords and cookies across Chrome, Brave, Firefox, and Edge; SSH keys; npm and GitHub tokens; AWS credentials; macOS Keychain entries; and cryptocurrency wallet data. Microsoft’s analysis adds scale to that: the framework probed for more than 100 environment variables, including AWS_ACCESS_KEY, AZURE_CLIENT_SECRET, and GCLOUD_SERVICE_KEY, alongside credential files like .npmrc, .aws/credentials, kubeconfig, id_rsa, .vault-token, and .docker/config.json.
One nuance is worth flagging rather than smoothing over: Microsoft notes that credential harvesting was “disabled in this build” even though the code paths targeting browser passwords and SSH keys were present in the framework. That’s a real discrepancy between what the malware was capable of and what it was confirmed to actively exfiltrate in the version analyzed — the kind of detail that matters for anyone deciding how urgently to rotate which credential. Attribution, meanwhile, stayed inconclusive: the payload self-identified as “M-RED-TEAM v6.4,” Wiz found possible but unconfirmed overlap with a framework researchers call Miasma, and the rentry.co paste slug matched naming patterns from an unrelated campaign — no firm attribution landed in any of the three writeups.
What should teams that depend on AsyncAPI tooling do now?
Assume any machine that ran npm install or imported one of the five versions between July 14’s publish windows is compromised, not just at risk. Because the payload fired on import rather than install, dependency-scanning approaches built around install-time behavior — including the defenses npm’s own v12 security overhaul was designed around — would not have caught this one. That overhaul turns off automatic install scripts by default; this attack never used an install script at all. It’s a live demonstration of the exact gap that npm v12’s critics warned was left open: runtime-triggered payloads that pass straight through script-blocking rules.
Do
- Audit every
pull_request_targetworkflow for whether it also checks out and executes the PR’s own head code - Treat an open, reviewed security fix as a live incident with an aging SLA, not a backlog item
- Rotate npm, GitHub, AWS, and SSH credentials on any machine that installed or imported the affected versions
- Check whether your own CI reuses the same secrets-plus-untrusted-checkout combination AsyncAPI’s workflow did
Don't
- Rely on
npm install --ignore-scriptsas a complete defense — this payload never touched an install script - Assume “harvesting disabled in this build” describes what a future republished version will do
- Let a proposed fix for a known secrets-exposure pattern sit in review for weeks without an owner
- Treat the fact that an attack hasn’t happened yet as evidence a flagged misconfiguration is low-risk
None of this required a zero-day. It required a known CI misconfiguration pattern, a proposed fix that outran the review queue by 58 days, and a registry that treats a stolen-but-valid publish token exactly like a legitimate one. AsyncAPI’s maintainers now face the unglamorous work of pulling five versions and explaining to 3 million weekly installs why a re-audit of their own supply chain just became mandatory. The fix that could have prevented all of it was sitting in the PR queue the entire time. Whether a credential escapes through an unmerged CI fix or a disputed forum listing of stolen cloud keys, the fallout runs on the same clock — until “reviewed” starts meaning “merged.”
Frequently asked questions
What GitHub Actions vulnerability did the attacker exploit?
A pull_request_target misconfiguration in AsyncAPI's manual-netlify-preview.yml workflow — a pattern Wiz calls a 'pwn request,' where a job runs with the base repository's secrets while still checking out untrusted pull-request code. That combination let PR #2155 execute inside a privileged context and steal a GitHub token.
How many npm packages were compromised, and how widely were they used?
Attackers published five trojanized versions across four @asyncapi packages — generator, generator-helpers, generator-components, and specs — between 07:10 and 08:30 UTC on July 14, 2026. Per Wiz, the four packages combined for over 3 million weekly downloads, making this a wide-blast-radius compromise of AsyncAPI's tooling.
What did the malicious payload actually do?
Per Wiz, a three-stage infection chain executed at import time, fetching an 8.25 MB encrypted bundle over IPFS and installing a 92,000-line malware framework with systemd persistence. It targeted browser-saved passwords, SSH keys, npm and GitHub tokens, AWS credentials, and cryptocurrency wallets across Linux, macOS, and Windows.
Why wasn't this vulnerability fixed before the attack happened?
A contributor flagged the workflow weakness and proposed a fix on May 17, 2026, but per Wiz and Chainguard it sat unmerged for 58 days before the July 14 exploit. Microsoft's own timeline traces the underlying proof-of-concept even earlier, to April 29 — months of visibility before anyone acted on it.
Does blocking npm install scripts stop this kind of attack?
No. Microsoft reports the payload executed at module-load (import/require) time rather than through an install script, so npm install --ignore-scripts — and even script-blocking defaults — would not have stopped it, because the malicious code only runs once a package is actually used in application code.
/* Comments */
Comments are offline right now — we reconnect automatically, nothing is lost.