All posts
Supply Chain Compromise

AsyncAPI npm: Docs Bot PAT, Registry-Scale Break

An org-wide asyncapi-bot PAT leaked from a Netlify-preview job, then republished four @asyncapi packages through the project's own legitimate CI

Securityv0 Intelligence Team OWASP: ASI06 sv0 finding: nhi_compromise
supply-chain nhi npm ci-cd asyncapi pull-request-target

The Incident

On 14 July 2026, an attacker published five trojanized versions across four npm packages in the @asyncapi organization: @asyncapi/generator@3.3.1, @asyncapi/generator-helpers@1.1.1, @asyncapi/generator-components@0.7.1, @asyncapi/specs@6.11.2-alpha.1, and @asyncapi/specs@6.11.2. The affected packages together see more than 2.25 million weekly downloads. The malicious versions were published between roughly 07:10 and 08:30 UTC and — because they were built and released by AsyncAPI’s own release workflow — the tarballs shipped to the registry with valid npm OIDC provenance attestations under the legitimate maintainer identity. All five versions have since been unpublished and clean successors restored. No CVE or GHSA had been assigned at the time of writing.

The payload is a second-stage loader that activates on require() or import — not at npm install — which means the common --ignore-scripts mitigation does not apply. Once imported, the loader retrieves a Miasma-lineage RAT from IPFS and initiates command-and-control over HTTP endpoints on 85.137.53.71:8080/8081/8091, with documented fallback channels over Nostr, Ethereum, libp2p, and BitTorrent DHT. Wiz’s analysis records the framework’s internal identifier as M-RED-TEAM v6.4 under the campaign tag miasma-train-p1, and independent trackers (Microsoft, GitGuardian, Palo Alto Unit 42) place the compromise in the ongoing Miasma / “Mini Shai-Hulud” family that traces back to the TeamPCP campaigns of earlier in 2026.

MITRE ATT&CK coverage: T1195.002 (Compromise Software Supply Chain: Compromise Software Dependencies and Development Tools), T1528 (Steal Application Access Token), T1204.002 (User Execution: Malicious File), T1071.001 (Application Layer Protocol: Web Protocols).

The Authority Path That Failed

The identity that carried authority at the moment of failure was asyncapi-bot, a non-human identity whose Personal Access Token was held by the CI environment as a workflow secret. Its stated purpose was narrow — rendering Netlify docs previews for pull requests. Its held scope was much broader: organization-wide repository write, including the ability to push directly to the release-triggering next branch. The manual-netlify-preview.yml workflow used a pull_request_target trigger while also checking out the untrusted PR head — the canonical “pwn request” pattern. When PR #2155 was processed — opened by throwaway account elzotebo999 amid a smokescreen of roughly 36 spam pull requests — attacker-controlled code executed inside a job that held the bot PAT and exfiltrated it to an external paste service. Approximately 100 minutes later, the attacker used that PAT to push commit 3eab3ec to the next branch. AsyncAPI’s own release workflow then built and published the trojanized tarballs to npm, producing valid OIDC provenance attestations for artifacts a maintainer had never authored.

The trust anchor that failed first was scope containment on a CI-resident non-human identity. Every downstream control — branch pushability, release-workflow authenticity, npm publish authority, OIDC provenance — behaved exactly as designed once the first boundary broke. Endor Labs’ post-incident analysis notes that the release branches (master, next) carried no push protection or required-review policy, so a valid asyncapi-bot PAT was sufficient authority to trigger the publishing pipeline. The workflow weakness itself was not novel: a proof-of-concept had been filed on 29 April 2026 and a remediation pull request opened on 17 May 2026 — the remediation was still unmerged when the attack landed on 14 July.

SecurityV0 Perspective

The failure was not provenance. It was authority. A credential intended to render a docs preview could write across the organization, reach a release branch, and trigger a trusted publishing workflow. Valid npm provenance then proved only that AsyncAPI’s workflow built the package — not that a maintainer intended the release.

Security teams should trace each CI identity from its justified task through every workflow, branch, and release path it can reach. The decisive question is not whether the token is legitimate. It is whether one attacker-controlled pull request can turn that token into publishing authority.

What To Do

  • Right-size every CI-resident PAT to the workflow that holds it. Replace organization-wide PATs held as GitHub Actions secrets with fine-grained tokens scoped to the single repository — and, where possible, the single branch — each workflow actually needs. A bot credential whose only job is rendering a docs preview must not carry release-branch write.
  • Ban pull_request_target plus untrusted checkout in the same job as any secret. Treat this combination as a policy violation, not a code-review question. Enforce with a repository ruleset or an org-wide GitHub Actions policy, and alert on any workflow that references github.event.pull_request.head.ref or head.sha inside a job that also references secrets.*.
  • Protect release-triggering branches with rules that survive a valid PAT. Require signed reviews and branch-protection status checks on main, master, next, and any other branch that triggers a package-publishing workflow. A valid asyncapi-bot PAT was sufficient to reach next here — the equivalent branch on your side must not be equally exposed.
  • Do not treat npm OIDC provenance as proof of maintainer intent. OIDC attestation confirms that a specific workflow in a specific repository produced a specific artifact; it does not confirm that the commit the workflow built was authored by a human maintainer. Include the head commit’s authorship, signature, and branch-arrival path in your provenance checks, and quarantine releases where any of those diverge from historical norms.
  • Detect import-time payloads, not just install-time scripts. --ignore-scripts does not stop code that runs on require(). Baseline the observable behavior of top-N direct and transitive dependencies at first import in a sandbox — outbound network to previously-unseen endpoints, IPFS gateway reads, filesystem access to credential paths — and alert on a delta from that baseline, per package version.

Sources