Virtual Round Table · Jul 22

View the event
A pull_request_target Workflow Hijacked the AsyncAPI npm Packages in Ninety Minutes
Cloud

A pull_request_target Workflow Hijacked the AsyncAPI npm Packages in Ninety Minutes

A misconfigured GitHub Actions workflow let attackers hijack the @asyncapi npm packages and ship an import-time botnet loader. The incident, and GitHub's response, is a direct warning for anyone who owns a CI/CD pipeline.

PublishedJuly 21, 2026
Read time6 min read
Share

A ninety-minute compromise of the AsyncAPI npm org

On July 14, attackers took over the @asyncapi npm organization and republished five malicious package versions across four names in roughly ninety minutes. Microsoft Threat Intelligence documented the campaign in a July 15 analysis, tracing every step from initial access to payload delivery. The affected releases were @asyncapi/specs in both its 6.11.2-alpha.1 prerelease and 6.11.2 stable form, @asyncapi/generator 3.3.1, @asyncapi/generator-components 0.7.1, and @asyncapi/generator-helpers 1.1.1. AsyncAPI tooling is widely used for event-driven API design, and reporting put the weekly download volume of the affected packages in the millions.

The timeline is tight enough to study as a case in operational speed. The malicious pull request landed at 05:08:58 UTC, the generator packages were republished around 07:10, the alpha build of specs published at 08:06:20, and the stable 6.11.2 followed at 08:30:09. By 08:49:22 UTC, the first downstream fetch into a Yarn cache was already observed. Less than four hours separated initial access from real-world consumption. For defenders, that window is the whole game, and it is far shorter than most manual review cycles.

The pull_request_target flaw at the root

The entry point was a GitHub Actions workflow, manual-netlify-preview.yml, in the asyncapi/generator repository's docs-preview automation. Pull request #2155 exploited a configuration that combined two unsafe choices. The workflow used the pull_request_target trigger, which runs the job in the base repository's security context with access to its secrets, and it checked out the untrusted head commit of the incoming pull request. Together those choices let attacker-controlled code execute with the repository's credentials, a pattern the community calls a "pwn request."

With repository secrets in hand, the attackers obtained the npm publishing credentials and pushed the poisoned versions. This is the failure mode that makes pull_request_target so dangerous: it exists to give preview automation access to secrets for forked pull requests, and pairing it with a checkout of untrusted code hands those secrets to anyone who opens a PR. For platform teams, the lesson is unambiguous. Any workflow using pull_request_target that also checks out PR head code is a live credential-theft vector, and it belongs at the top of a CI/CD audit list.

Import-time execution raised the blast radius

What set this campaign apart from routine npm supply-chain attacks was the execution model. Most poisoned packages run their payload through a postinstall hook, which means an install with scripts disabled avoids the trigger. This loader executed at module-load time, running the moment any consuming code imported or required the package. Microsoft Threat Intelligence stated it directly: "Unlike the more common postinstall-hook supply-chain pattern, this campaign executes at module-load (import/require) time. When any consuming build or application imports a poisoned package, the injected block runs immediately."

Because @asyncapi/specs is a transitive dependency of numerous AsyncAPI tooling packages, the exposure reached well beyond direct users. Any developer workstation, CI/CD pipeline, container build, or production service that resolved and imported an affected version during the exposure window ran the loader. That breadth is the reason import-time execution is so much more dangerous than the postinstall pattern most teams have tuned their defenses against. The common mitigation of installing with scripts disabled offered no protection here at all.

What the payload actually did

The first-stage loader pulled a second stage from IPFS: an encrypted sync.js file of roughly 8.2 MB. It used three cryptographic layers, including HKDF-SHA256 key derivation and AES-256-GCM encryption, though the key material was statically embedded, which means analysts could recover the runtime without executing it. The decrypted payload was a remote-access framework the researchers labeled the Miasma runtime, tagged M-RED-TEAM v6.4, with persistence mechanisms and full RAT capabilities.

The command-and-control design is the part that should worry infrastructure teams. The malware carried multiple fallback channels including Nostr, Ethereum, BitTorrent DHT, libp2p, and IPFS. That redundancy makes takedown and network blocking far harder than a hardcoded domain would. It also signals a level of engineering investment that puts this campaign well past opportunistic typosquatting. The attackers built resilience into the botnet from the start, which is a sign of where npm-targeted threats are heading.

Remediation that actually works

Microsoft's guidance is specific, and the first point overturns a common assumption. Purging npm and Yarn caches is required, and running npm install with --ignore-scripts does not mitigate this attack because the payload never relied on install scripts. Teams should pin @asyncapi/specs to 6.11.1 or earlier and roll the other affected packages back to their prior minor versions. Hunting for a stray sync.js in Node.js masquerade directories helps confirm whether a system executed the loader.

The network and credential steps matter just as much. Microsoft advises blocking outbound connections to 85.137.53.71 on ports 8080, 8081, and 8091, and rotating every credential reachable from affected systems. That last instruction is the expensive one, and it is unavoidable given the loader ran with whatever secrets its host held. Any CI runner that pulled an affected version during the window should be treated as compromised, and its tokens rotated. Skipping credential rotation leaves a door open long after the packages are cleaned up.

The ecosystem response: cooldowns and hardened defaults

The incident arrived in the same week GitHub shipped defenses aimed at this class of attack. On July 14, Dependabot introduced a default three-day cooldown before it opens version-update pull requests, meaning a newly published release must sit on its registry for at least three days before automation pulls it in. Security updates are exempt, so genuine fixes still flow immediately. The default applies across supported ecosystems on github.com and is planned for GitHub Enterprise Server 3.23, and it needs no configuration to take effect.

Reporting on July 20 described GitHub backporting secure-by-default changes to GitHub Actions to close the pwn-request window that the AsyncAPI attackers walked through. The combined message to CI/CD owners is clear. Audit every pull_request_target workflow that checks out untrusted code, adopt a package-age floor so freshly published versions cannot land in a build within minutes, and move publishing credentials onto short-lived, scoped tokens. This campaign showed how little time defenders get once a trusted package is poisoned, and the ecosystem's response gives teams concrete controls to shrink that window.

Tagged#news#engineering#software-engineering#devops#platform-engineering#architecture#infrastructure#asyncapi#npm#supply-chain#github-actions#dependabot