The AsyncAPI npm Compromise: Provenance Proves the Pipeline, Not the Commit
Poisoned @asyncapi packages shipped through a legitimate trusted-publishing pipeline with valid provenance, and ran at import time, past --ignore-scripts. What to check this week.
Umut Korkmaz
Published: 4 min read
On this page6
On July 14, poisoned versions of four packages in the @asyncapi npm scope were published, and they went out through the project's own release pipelines. On July 15 Microsoft published a detailed teardown of the AsyncAPI compromise. Two details matter to anyone who ships or consumes JavaScript: the packages carried valid provenance, and the payload runs when the module is imported, so --ignore-scripts does nothing to stop it.
What was published
According to Microsoft, five versions across four package names:
@asyncapi/specs6.11.2-alpha.1 and 6.11.2@asyncapi/generator3.3.1@asyncapi/generator-components0.7.1@asyncapi/generator-helpers1.1.1
Microsoft puts the three generator packages at around 07:10 UTC, and the npm registry shows the specs alpha at 08:06 UTC and 6.11.2 at 08:30 UTC. The known-good versions it points to are specs 6.11.1 or earlier, generator 3.3.0, generator-components 0.7.0 and generator-helpers 1.1.0.
How a preview workflow became a release key
The entry point was a GitHub Actions workflow in asyncapi/generator, manual-netlify-preview.yml. It ran on pull_request_target, which puts the job in the base repository's security context, and it checked out the pull request's untrusted head commit. Checkout credentials stayed in the local Git config until post-job cleanup, and the job had steps that referenced repository secrets. The attacker opened a pull request whose content was designed to fetch and evaluate remote JavaScript. Microsoft says this exposed the asyncapi-bot personal access token, while noting the public logs do not show exactly how it was obtained. Later pushes were authenticated as asyncapi-bot.
The pushes went to auto-publish branches, so there was no need to compromise npm: the commits triggered the real release workflows, which published through npm trusted publishing with GitHub OIDC. Microsoft notes the provenance attestations were valid and correctly named the legitimate repositories, commits and workflows, even though the triggering commits were unauthorized.
That is the uncomfortable part. Provenance answers "which pipeline built this?" It does not answer "should that pipeline have run on this commit?" Once an attacker can push to the branch a release workflow watches, OIDC publishing becomes a very convincing way to launder a malicious build.
Why --ignore-scripts did not help
The usual advice for npm incidents is --ignore-scripts, because the classic attack lives in a postinstall hook. Microsoft calls that the more common pattern, and this campaign avoided it: the packages declared no install hooks. The loader sits in each package's entry path (index.js for @asyncapi/specs). The npm docs describe ignore-scripts as stopping npm from running scripts specified in package.json files; it has no say over code that runs on require or import. Microsoft is explicit: do not rely on it here.
On import, the injected block spawns a hidden, detached Node child process, which downloads an encrypted bundle of about 8.2 MB from IPFS, and that second stage decrypts a runtime Microsoft calls Miasma. In the build they analysed, persistence and C2 were active and the credential-harvesting module was present but disabled. The target list is still worth reading: GITHUB_TOKEN, NPM_TOKEN, cloud keys, VAULT_TOKEN, .npmrc, SSH keys, kubeconfig, and also ANTHROPIC_API_KEY and OPENAI_API_KEY. I run several coding agents in parallel every day, and a machine set up like that tends to have exactly these keys in its environment.
Persistence differs per OS: a HKCU Run key on Windows, a systemd user unit on Linux, and shell RC injection into .zshrc, .bashrc or .bash_profile on macOS.
What I would check on Monday
If you consume these packages:
- Search lockfiles for the five versions and pin to the known-good ones.
- Purge npm and Yarn caches on developer machines and build hosts, so a cached tarball cannot come back.
- If anything imported a bad version, rotate credentials from a clean host. Microsoft's at-risk list covers developer workstations, CI/CD pipelines, container builds and production services.
- Alert on connections to
85.137.53[.]71on ports 8080, 8081 and 8091, and consider blocking public IPFS gateways at the perimeter. - Look at
min-release-age, added in npm CLI 11.10.0 and recommended by Microsoft. It only installs versions available for more than a given number of days, so a version published hours earlier would not be picked.
If you publish packages:
grep -rn "pull_request_target" .github/workflows/
- Treat any job that combines
pull_request_targetwith checking out the PR head as a vulnerability until proven otherwise. - Replace long-lived bot PATs with narrower, shorter-lived credentials, and do not leave checkout credentials persisted in jobs that touch untrusted code.
- Put the publish job behind a protected environment with required approval, so a push alone cannot ship a release. Microsoft lists token scopes, workflow approvals, protected environments, release provenance and anomaly detection around automated publishing as the areas to review.
My take
Trusted publishing is still better than a long-lived npm token sitting in a repository secret, and I would not tell anyone to turn it off. But this incident shows it moves the trust boundary rather than removing it. The question stops being "who holds the npm token" and becomes "who can make the release workflow run". For a lead, that means workflow files deserve the same review as production code, and a release should need a human decision, not just a commit on the right branch. Provenance tells you where a package came from. It does not tell you whether it should exist.
Sources
Primary references used for this post.
- microsoft.com/en-us/security/blog/2026/07/15/unpacking-asyncapi-npm-supply-chain-compromise-import-time-payload-delivery (opens in a new tab)
- registry.npmjs.org/@asyncapi%2fspecs (opens in a new tab)
- docs.npmjs.com/cli/v11/using-npm/config (opens in a new tab)
- github.com/npm/cli/releases/tag/v11.10.0 (opens in a new tab)