On July 14, 2026, an attacker published five malicious versions across four @asyncapi npm packages. They did not steal a maintainer's npm token. They obtained push access to two AsyncAPI repositories and let the projects' own GitHub Actions release pipelines do the publishing, which meant the packages arrived on the registry with valid OIDC provenance attestations.
The payload contained no install scripts. It executed on require(), which meant the mitigation the entire ecosystem had just adopted (npm 12 disabling lifecycle scripts by default, shipped six days earlier) did nothing. Two of the industry's most-recommended supply chain controls were in place and neither one was in the attack path.
This is the counterpart to the axios supply chain attack we covered earlier this year. That one was a hijacked maintainer account delivering a RAT through a postinstall hook, and the fix was lockfile discipline plus script blocking. This one routes around both. It is worth understanding precisely why.
* Every technical detail below is drawn from the published analyses by Microsoft Security and StepSecurity. All external claims are linked inline and listed in the numbered Sources section at the end.
What happened on July 14?
The attack ran for roughly four hours across two AsyncAPI repositories. According to StepSecurity's analysis, the malicious commits were authored under a placeholder identity, “Your Name” with the address you@example.com and the GitHub login invalid-email-address. The generator packages were exposed for 4 hours and 2 minutes; specs 6.11.2 for 2 hours and 48 minutes.
Incident timeline
July 8, 2026
npm 12 ships with dependency lifecycle scripts disabled by default. GitHub had called those scripts the single largest code-execution surface in the npm ecosystem.
July 14, 2026 - 06:58 UTC
The attack begins against the AsyncAPI generator repository.
July 14, 2026 - 07:10 UTC
Three generator packages are published to npm with malicious code, through the project's own release workflow.
July 14, 2026 - 08:06 to 08:30 UTC
Two @asyncapi/specs versions are published from the second repository.
July 14, 2026 - 11:12 to 11:18 UTC
All five malicious versions are unpublished from npm. Generator exposure window: 4h 2m. Specs 6.11.2 exposure window: 2h 48m.
July 15, 2026
Microsoft publishes its analysis of the compromise and the import-time delivery mechanism.
Affected versions
| Package | Malicious version | Last safe version |
|---|---|---|
| @asyncapi/generator | 3.3.1 | 3.3.0 |
| @asyncapi/generator-helpers | 1.1.1 | 1.1.0 |
| @asyncapi/generator-components | 0.7.1 | 0.7.0 |
| @asyncapi/specs | 6.11.2 | 6.11.1 |
| @asyncapi/specs | 6.11.2-alpha.1 | 6.11.1 |
How did malware get a valid provenance attestation?
The attestation was valid because it was accurate. npm provenance answers exactly one question: which repository, commit, and workflow produced this artifact. In this case the honest answer was the real AsyncAPI repository, running the real release workflow, on a commit that genuinely existed in the branch. Every one of those statements was true. None of them was reassuring.
StepSecurity puts the limitation plainly: the packages carry legitimate SLSA provenance attestations “proving only that the project's authorized workflow produced them, not that the triggering commits were legitimate.” Microsoft's writeup reaches the same conclusion, noting the attestations accurately identified the legitimate repositories, commits, and workflows even though the triggering commits were unauthorized.
This matters because provenance has been marketed, informally, as a trust signal. It is a build-integrity signal. It closes the gap between “the source repository” and “the published tarball.” It says nothing whatsoever about the gap between “a maintainer intended this” and “this reached the source repository.” The attacker simply moved one step upstream of where the control sits.
Provenance is a chain-of-custody receipt, not a code review
Keep using it. A missing or mismatched attestation is still a strong negative signal. Just stop treating a green checkmark as evidence that the code is safe, and never let it substitute for branch protection on the branch your release pipeline builds from.
Why did blocking install scripts not help?
Because there were no install scripts to block. The timing here is almost unkind. In its June 9, 2026 changelog announcing the change, GitHub called install-time lifecycle scripts “the single largest code-execution surface in the npm ecosystem,” noting that a single compromised package anywhere in the tree can run arbitrary code on a developer machine or CI runner. npm 12 shipped on July 8, 2026 with allowScripts defaulting to off, so preinstall, install, and postinstall scripts from dependencies no longer run unless explicitly approved. Git dependencies (--allow-git) and remote tarball dependencies (--allow-remote) both default to none in the same release.
Six days later, the AsyncAPI dropper went around it. The obfuscated loader sat in the module body and fired when the library was imported. That is not an exotic technique; it is simply how JavaScript modules work. Anything that runs at the top level of a module executes the first time something requires it, which in a normal project means during your build, your test run, or your application boot.
// What defenders expected to find, and did not:{"name": "@asyncapi/generator","version": "3.3.1","scripts": {// No preinstall. No install. No postinstall.// Nothing here to block.}}// Where the dropper actually lived: the module body.// It runs the moment anything require()s or import()s the package,// which happens in your build, your tests, and your app startup.spawn("node", ["-e", stage2], {detached: true, // survives the parent processstdio: "ignore", // no output in your CI logswindowsHide: true, // no console window});// npm install --ignore-scripts does not help here.// npm 12's allowScripts default does not help here.// The code runs because you imported the library, as intended.
The mitigation moved, so the attacker moved
This is what defense evolution looks like in practice. Install-script blocking is still worth having; it retires an entire generation of attacks, including the axios one. But a control that closes one execution path pushes attackers to the next one, and for a library you actually import, the next one is trivially available.
What did the payload actually do?
The first stage spawned a hidden, detached Node process with output suppressed, then fetched an encrypted second-stage runtime of roughly 8.2 MB (named sync.js) from IPFS, decrypted it, and executed the Miasma remote access framework. Miasma installed persistence and established command and control.
The interesting part is the resilience of the C2 design. Primary control ran over 85.137.53.71 on ports 8080 for commands, 8081 for uploads, and 8091 for proxy management. Fallback channels included Nostr relays, the BitTorrent DHT via router.bittorrent.com:6881, and an Ethereum mainnet contract. Blocking one IP address does not retire this implant.
One module deserves separate attention. StepSecurity documents an ai-tool-poisoner.js component and advises treating AI coding assistant sessions on an affected machine as potentially compromised. That is the same trust boundary the Azure DevOps MCP disclosure attacks from the other direction: one poisons what the agent reads from a pull request, the other poisons the agent's local tooling from a dependency. Both end with your agent following someone else's instructions using your credentials.
Which assumptions did this attack break?
Four widely held beliefs about npm security failed here at the same time. Each one is a reasonable rule of thumb that happens to describe the previous generation of attacks rather than this one.
| What we assumed | What happened |
|---|---|
| A signed provenance attestation means the package is trustworthy | The attestation was valid and accurate. It proved the package came from the real repository, commit, and workflow. It said nothing about whether the commit was authorized. |
| Blocking install scripts stops malicious packages | The packages had no install scripts. The dropper ran on require(), so the new npm 12 default and --ignore-scripts both miss it entirely. |
| A compromised maintainer account is the thing to watch for | No npm token was stolen. The CI pipeline was the credential. Push access to a branch was enough to reach publish. |
| A short exposure window means low risk | Four hours is long enough for CI to resolve a caret range, install the version, import it, and execute. Lockfiles and caches then keep it around. |
How do I check whether I was affected?
Check two things: whether your lockfile ever resolved one of the five versions, and whether the second-stage artifact is sitting on disk. All five versions have been unpublished, so a fresh install will not reach them, but a cached lockfile from July 14 still can. Note that a build during the window is enough. You do not need to have shipped anything.
#!/usr/bin/env bash# Check whether you resolved any of the malicious versions,# then look for the on-disk artifact the dropper leaves behind.set -uo pipefailBAD=("@asyncapi/generator@3.3.1""@asyncapi/generator-helpers@1.1.1""@asyncapi/generator-components@0.7.1""@asyncapi/specs@6.11.2""@asyncapi/specs@6.11.2-alpha.1")echo "== Lockfile check =="for pkg in "${BAD[@]}"; doname="${pkg%@*}"; version="${pkg##*@}"if grep -q "\"${name}\": \"\?${version}" package-lock.json 2>/dev/null; thenecho "HIT: ${pkg} present in package-lock.json"fidoneecho "== Drop file check =="for path in \"$HOME/.local/share/NodeJS/sync.js" \"$HOME/Library/Application Support/NodeJS/sync.js" \"$LOCALAPPDATA/NodeJS/sync.js"do[ -f "$path" ] && echo "HIT: dropper artifact at $path"doneecho "Done. Any HIT means: rotate credentials and rebuild the host."
If you get a hit
Treat it as code execution on that host, not as a dependency to bump. The published remediation guidance is:
- Delete and regenerate lockfiles, and purge npm and Yarn caches.
- Remove the
sync.jsdrop file from the NodeJS application data directory for your platform. - Rotate every credential the host could reach: npm tokens, GitHub personal access tokens, SSH keys, and cloud credentials.
- Audit build logs for outbound traffic to IPFS gateways, the C2 address, DHT bootstrap nodes, or Nostr relays.
- Rebuild affected machines from a clean baseline rather than cleaning them in place.
- Treat AI coding assistant sessions on the host as potentially compromised, given the AI tool poisoning module.
What actually defends against this class of attack?
Split the answer by which side of the registry you are on. If you publish packages, your job is to make push access insufficient to reach publish. If you consume packages, your job is to make a four-hour exposure window something your pipeline sleeps through.
1Fix the trigger that leaked the token
Microsoft attributes the initial access to a workflow using pull_request_target, which runs in a privileged context with repository secrets available. Checking out the pull request head under that trigger runs a stranger's code with your tokens in the environment. This is a well-documented footgun and it is still everywhere.
# The root cause, per Microsoft's analysis: pull_request_target# runs in a PRIVILEGED context with access to repository secrets.# Checking out the pull request head under that trigger executes# attacker-controlled code with your tokens in the environment.# VULNERABLEon:pull_request_target:jobs:build:steps:- uses: actions/checkout@v4with:ref: ${{ github.event.pull_request.head.sha }} # attacker code- run: npm ci && npm run build # with secrets# SAFE: untrusted code runs under pull_request, which has no secrets.on:pull_request:jobs:build:permissions:contents: readsteps:- uses: actions/checkout@v4- run: npm ci --ignore-scripts && npm run build# If you genuinely need pull_request_target (to label or comment),# never check out the head ref, and never run the PR's code.
2Publishers: put a human between push and publish
Trusted publishing removed the long-lived npm token, which is a real improvement. It also means the branch is now the credential. If your release workflow publishes on every push to a branch, then push access to that branch is publish access, and provenance will faithfully attest to whatever it builds. Protect the branch, gate the publish job behind a GitHub Environment with required reviewers, and pin actions to commit SHAs.
# Make "push access" insufficient to reach publish.# 1. Protect the branch your release workflow builds from.# The AsyncAPI publish pipeline built from 'next'.# Require a review from a code owner on every push to it.# 2. Gate publish behind a GitHub Environment with required reviewers.jobs:publish:environment:name: npm-production # add required reviewers in repo settingspermissions:id-token: write # OIDC trusted publishingcontents: readsteps:- uses: actions/checkout@v4- run: npm ci --ignore-scripts- run: npm publish --provenance --access public# 3. Pin every action to a full commit SHA, not a tag.# - uses: actions/checkout@b4ffde6... # v4.2.2# 4. Put an egress allowlist on the runner. The AsyncAPI dropper# had to reach an IPFS gateway to fetch stage two. A default-deny# egress policy turns a full RAT install into a failed request.
3Consumers: refuse to be the first to install anything
This is the single highest-leverage control for a consuming team, and it is one line of config. Malicious versions get caught and unpublished in hours. A cooling-off period converts almost every one of these incidents into something you read about rather than something you respond to. It would have covered all five AsyncAPI versions with days to spare.
# .npmrc - slow down the blast radius on the consuming side.# Refuse any version published in the last N days. The five malicious# AsyncAPI versions were live for under four hours; a cooling-off# period means your CI would never have seen them.# Requires npm v11.10.0+min-release-age=7# npm 12 blocks dependency lifecycle scripts by default. Set this# explicitly so older npm versions in your fleet behave the same way.# (In npm 12 the allowlist of packages permitted to run scripts is# managed with 'npm approve-scripts', not with this key.)ignore-scripts=true# Always npm ci in CI, never npm install. ci installs exactly what# the lockfile says and fails if package.json and the lock disagree.
4Default-deny egress on build runners
The dropper was a stager. It had to reach an IPFS gateway to fetch the 8.2 MB payload that did the actual work. An egress allowlist on your runners (registry, source control, your own artifact store, nothing else) turns full remote access into a failed HTTP request and a log line. StepSecurity reports that its Harden-Runner tooling detected this attack precisely by capturing outbound connection attempts. Runtime network visibility is what caught it, not static analysis of the package.
The principle: assume a malicious dependency will eventually execute, and constrain what it can reach
Every control that tried to decide whether the package was trustworthy failed here, because the package looked exactly like a legitimate release. The controls that would have worked never asked that question. They delayed the install, restricted the network, or required a second person to approve a publish.
What this says about the next one
Three npm incidents in the space of a few months tell a consistent story. The axios compromise took a maintainer account and used a postinstall hook. The Claude Code source map leak needed no attacker at all, just a default nobody had turned off. This one took a CI trigger and used the module system. In each case the failure was at a seam between systems that each behaved correctly on their own.
The uncomfortable part is that the controls being retired here are the ones we recommend. Install-script blocking is good. Provenance is good. Both are worth adopting today. Neither is a decision procedure for “is this package safe,” and the moment we treat them as one, we stop building the controls that assume the answer is no. If your team is leaning harder on AI agents to write and review this code, the agent-side version of this same problem is worth reading next, and the curl project's experience with AI-generated vulnerability reports is a useful counterweight on the noise side.
For the broader maintainer perspective on why these projects are load-bearing and chronically under-resourced, our piece on maintaining open source covers the part of this that no config file fixes. And if you are picking the agent stack that will be running npm ci on your behalf, our comparison of Cursor, Claude Code, and OpenCode and our guide to writing a CLAUDE.md cover where to encode these limits so they are enforced by default.
Sources and citations
- Microsoft Security Blog, “Unpacking the AsyncAPI npm supply chain compromise and import-time payload delivery,” July 15, 2026 (root cause, affected versions, provenance analysis, payload behavior). microsoft.com
- StepSecurity, “Coordinated AsyncAPI Supply Chain Attack: Miasma RAT Delivered via Compromised CI/CD Pipelines in Two Repositories,” July 2026 (timeline, exposure windows, C2 infrastructure, remediation steps). stepsecurity.io
- GitHub Changelog, “Upcoming breaking changes for npm v12,” June 9, 2026 (primary source for the allowScripts, --allow-git, and --allow-remote defaults, the npm approve-scripts command, and the “single largest code-execution surface in the npm ecosystem” description). github.blog
- The Hacker News, “npm 12 Disables Install Scripts by Default to Reduce Supply Chain Risk,” July 2026 (npm 12 ship date of July 8, 2026). thehackernews.com
- npm CLI documentation, configuration reference (
min-release-age, added in npm v11.10.0, andignore-scripts). docs.npmjs.com - Palo Alto Networks Unit 42, “The npm Threat Landscape: Attack Surface and Mitigations,” updated July 15, 2026. unit42.paloaltonetworks.com
- GitHub Docs, security hardening guidance for GitHub Actions, including the
pull_request_targettrigger and untrusted input. docs.github.com - npm Docs, generating provenance statements and trusted publishing (what an attestation does and does not assert). docs.npmjs.com
Indicators of compromise are reproduced from the published StepSecurity and Microsoft analyses for detection purposes. Verify against those sources before acting on them, and check for updates published after July 15, 2026.
Frequently asked questions
Which AsyncAPI packages were compromised?
Five versions across four packages: @asyncapi/generator 3.3.1, @asyncapi/generator-helpers 1.1.1, @asyncapi/generator-components 0.7.1, and @asyncapi/specs 6.11.2 plus 6.11.2-alpha.1. The last safe versions are 3.3.0, 1.1.0, 0.7.0, and 6.11.1 respectively. All five malicious versions were unpublished from the npm registry between 11:12 and 11:18 UTC on July 14, 2026.
How did malicious packages get a valid npm provenance attestation?
Because the attestation was telling the truth. The attacker did not steal an npm token. They obtained push access to the repository and let the project's real GitHub Actions release workflow publish the packages through npm's OIDC trusted publishing. The resulting attestation accurately named the legitimate repository, commit, and workflow. Provenance proves where a package was built, not that the code going in was authorized.
Does npm install --ignore-scripts protect against this attack?
No. The malicious packages contained no preinstall, install, or postinstall scripts at all. The obfuscated dropper lives in the module body and executes when the library is imported or required, which happens during your build, your tests, or your application startup. Disabling install scripts, including the new npm 12 default, does nothing against an import-time payload.
What did the AsyncAPI payload do once it ran?
It spawned a hidden detached Node process, downloaded an encrypted runtime of roughly 8.2 MB named sync.js from IPFS, decrypted it, and executed the Miasma remote access framework. Miasma installed persistence and connected to command and control at 85.137.53.71 on ports 8080, 8081, and 8091, with fallback channels over Nostr relays, the BitTorrent DHT, and an Ethereum mainnet contract.
How did the attacker get push access to the AsyncAPI repositories?
Microsoft's analysis attributes it to a misconfigured GitHub Actions workflow using the pull_request_target trigger, which executed attacker-controlled pull request code in a privileged context and exposed the asyncapi-bot personal access token. That token was then used to push to the next branch, which the release pipeline builds from.
What should I do if my lockfile contains one of the affected versions?
Assume code execution on any machine that installed and imported the package. Delete and regenerate lockfiles, purge npm and Yarn caches, and remove the sync.js drop file from the NodeJS application data directory for your OS. Rotate every credential the affected machine could reach, including npm tokens, GitHub personal access tokens, SSH keys, and cloud credentials. Because the payload included an AI tool poisoning module, treat AI coding assistant sessions on that machine as potentially compromised too.
Related resources
Hidden PR Comments Hijack AI Code Reviewers
The Azure DevOps MCP confused deputy flaw, and the four layers that keep a prompt injection from reaching anything worth taking.
The Axios Supply Chain Attack
The previous generation of this attack: a hijacked maintainer account and a postinstall RAT, plus the .npmrc hardening that stops it.
Claude Code's 59.8 MB Source Map Leak
What one unset bundler default exposed, and the four-layer packaging config that would have caught it before publish.
Fake OpenAI Privacy Filter on Hugging Face
A typosquatted repo hit number one trending in 18 hours with 244K downloads. The attack chain and seven verification checks.
Fewer Dependencies in Your Document Pipeline
TurboDocx replaces a stack of document generation and signing libraries with one audited, API-first service. Less code you did not write running in your build.
