The AsyncAPI npm supply chain attack of July 14, 2026 trojanized four packages — anchored by @asyncapi/specs and its 2.66 million weekly downloads — with a payload that executes the moment the module is imported. No install script. No postinstall hook. Nothing for npm 12's new default protections, shipped just six days earlier, to block.
That timing is the story. On July 8, npm 12 turned off install scripts by default and the ecosystem exhaled — the attack vector behind most of 2025's headline compromises was finally closed at the platform level. Six days later, an attacker demonstrated the counter-move: skip the lifecycle hooks entirely and detonate at module-load time, when a build, test suite, CLI run, or production app first calls require or import on the poisoned code.
This guide covers what happened hour by hour, why install-time controls structurally cannot catch this class of attack, the three-stage anatomy of the malware, the CI misconfiguration that made it possible, and — the part you can act on — a defense-layer matrix mapping every common mitigation against the specific attack stages it would or would not have interrupted.
- 01Four packages, five versions, one UTC morning.@asyncapi/specs (2.66M weekly downloads), @asyncapi/generator, generator-components, and generator-helpers were trojanized on July 14, 2026 and unpublished within roughly four hours — fast, but wide enough to matter at this download volume.
- 02The payload runs at import, not install.No lifecycle script is declared at all. npm install --ignore-scripts and npm 12's new default install-script blocking do nothing — the malware fires when a build, CI job, or app first imports the module.
- 03Valid provenance, poisoned release.The packages were published through npm trusted publishing with legitimate SLSA attestations. The root cause was a pwn request — a misconfigured pull_request_target workflow that leaked a privileged token weeks after the risk was flagged internally.
- 04Second major compromise in a single week.Jscrambler's npm packages were compromised on July 11 via a classic preinstall-hook stealer; AsyncAPI followed on July 14 with an import-time payload. Unit 42 tracks at least six major npm campaigns in 2026 before this one.
- 05Only layered controls interrupt this chain.Lockfile pinning with npm ci, a 24-72 hour dependency cooldown, CI egress allowlisting, and credential rotation each cover a stage that install-script blocking and provenance verification demonstrably do not.
01 — The IncidentFour packages, one morning, 2.66M weekly downloads.
On the morning of July 14, 2026 (UTC), five malicious versions landed across four packages in the @asyncapi npm namespace: @asyncapi/specs v6.11.2 and v6.11.2-alpha.1, @asyncapi/generator v3.3.1, @asyncapi/generator-components v0.7.1, and @asyncapi/generator-helpers v1.1.1. Socket.dev's registry-metadata inspection puts the first publish at 07:10:42 UTC and the last — the stable specs release — at 08:30:09 UTC. Microsoft's security team published the deepest incident analysis the following day.
The blast radius is what separates this from a typosquat. @asyncapi/specs alone sees 2.66 million downloads a week, and it is also pulled in transitively by @asyncapi/parser (roughly 970,000 weekly downloads) through a ^6.11.1 semver range — meaning the poisoned 6.11.2 satisfied countless installs automatically, with no direct upgrade action by anyone. Combined weekly downloads across the four packages run to two million-plus, by some counts over three million; outlets round differently, so treat the per-package specs figure as the anchor stat.
@asyncapi/specs weekly downloads
Also consumed transitively by @asyncapi/parser (~970K/week) via a ^6.11.1 range — the poisoned patch release satisfied existing installs with zero action from maintainers downstream.
First publish to full unpublish
First malicious publish 07:10:42 UTC; npm and AsyncAPI maintainers pulled all five versions by roughly 11:18 UTC. StepSecurity puts per-package exposure between about 2h48m and 4h03m.
Declared by the malware
No preinstall, install, or postinstall hook exists in any trojanized version. The implant executes at module load — which is precisely what hook-focused scanners and install-script blockers never inspect.
The roughly four-hour window sounds reassuring until you multiply it against the download rate. At 2.66 million weekly downloads, the specs package averages hundreds of installs a minute — and every CI pipeline that ran npm install against a floating range in that window, then executed a build that imported the module, ran the payload. Speed of unpublish limits the population; it does not identify who in that population was hit.
Weekly downloads in July's blast radius · reported baselines
Source: StepSecurity, cybersecuritynews.com, July 2026 reported baselines02 — The npm 12 GapWhy npm 12's defenses never fired.
npm 12 shipped on July 8, 2026 with the most significant default hardening in the registry's history: allowScripts off by default — blocking preinstall, install, and postinstall lifecycle scripts plus implicit node-gyp builds — and git-dependency and remote-tarball resolution disabled unless explicitly allowed. We covered the full mechanics in our npm 12 install-script defaults guide. Those defaults are real progress against the attack pattern that dominated 2025.
They are also completely orthogonal to what happened on July 14. The AsyncAPI malware declares no lifecycle script of any kind — so there is nothing for npm 12 to block, and npm install --ignore-scripts, the veteran mitigation in every hardening guide, is equally inert. The payload triggers the first time the poisoned module is actually imported: a generator build, a CI test run, a CLI invocation, or an application server booting in production.
This is the paragraph worth internalizing as a trend, not a one-off. Platform defaults are public documents: the moment npm 12 announced that install scripts were blocked by default, every capable attacker knew exactly which technique was depreciating and which adjacent technique — runtime execution inside legitimate module code — was untouched. The AsyncAPI campaign landing six days after the npm 12 release is the clearest demonstration yet that supply-chain defenses and attacker innovation move in the same news cycle. Defenses that respond to last quarter's attack pattern buy you exactly one quarter.
03 — Attack AnatomyThree stages, from one line to a 744-module framework.
Socket.dev's stage-by-stage breakdown — corroborated by Microsoft and Datadog Security Labs — shows a deliberately minimal footprint inside the npm tarball and a deliberately maximal capability at the end of the chain.
Import-time implant
A single obfuscated JavaScript statement injected into one legitimate source file per package — index.js in specs, src/utils.js in generator-helpers — using hex-like variable names and a base64 string table, with a main() call executed at module load.
IPFS loader
Fetches an encrypted second stage from an IPFS gateway and writes it to a path disguised as Node.js runtime state — sync.js under %LOCALAPPDATA% on Windows, ~/Library/Application Support on macOS, ~/.local/share on Linux.
Miasma framework
A tasking framework bundling roughly 744 modules per Socket.dev's static analysis: multi-channel C2, platform-specific persistence, and credential theft spanning well over 100 environment variables and file types.
Stage 1 spawns a detached, hidden Node.js child process — spawn("node", ["-e", stage2], { detached: true, stdio: "ignore", windowsHide: true }) — the moment the poisoned module loads, then returns control to the legitimate package code. The developer or CI job sees a normal, green run. Using IPFS for stage 2 is the clever part: because the npm tarball only ever contained a small downloader, the operator can update or replace the final payload at will without republishing anything to npm.
The Miasma framework itself is built for resilience. Beyond REST/HTTP beaconing to a hardcoded IP, Socket.dev, StepSecurity, and Datadog document code paths for Nostr relays, Ethereum smart-contract lookups, libp2p, BitTorrent DHT bootstrap nodes, and mDNS discovery — most shipped present but disabled in the observed "safe mode" configuration. Credential targeting spans GitHub, npm, AWS, Azure, GCP, Docker, and Kubernetes tokens, .npmrc and .aws/credentials files, SSH keys, browser-saved passwords and cookies across Chrome, Brave, Firefox, and Edge, and the macOS Keychain. StepSecurity additionally reports an ai-tool-poisoner.js component targeting locally installed AI coding assistant sessions and config — a detail from one source, not independently confirmed by Microsoft or Socket.dev, but consistent with the July 11 Jscrambler stealer's documented interest in AI tool configs. To be clear: that is about local config files on an already-compromised machine, not a compromise of any AI tool itself.
04 — Root CauseA pwn request — and provenance that told the truth about a lie.
No npm account was phished and no maintainer went rogue. The entry point, documented by Chainguard and corroborated by Socket.dev, StepSecurity, and Wiz, was a misconfigured pull_request_target GitHub Actions workflow in the asyncapi/generator repository — a "pwn request." The workflow checked out attacker-controlled PR code while running with base-repo secrets access, exposing a highly privileged personal access token belonging to the asyncapi-bot account. The attacker opened roughly three dozen decoy pull requests in the hours before the exploit PR, muddying which one mattered.
The sharpest detail: the vulnerable pattern was known. Wiz traces a researcher proof-of-concept to April 2026 with a proposed fix left unmerged, and Chainguard describes the pattern as flagged internally weeks before the attack. Sources differ on the precise dates, but they agree on the shape — a multi-week window between internal warning and external exploitation.
"Both attacks are CI/CD pipeline compromises, not stolen npm tokens or malicious maintainers."— Rohan Prabhu, StepSecurity
That framing — covering both AsyncAPI and the July 11 Jscrambler incident — matters because it moves the failure domain. The malicious packages were published through npm's trusted publishing (GitHub OIDC) mechanism and carried valid SLSA provenance attestations. The attestations were not forged. They correctly proved the packages came from the project's authorized CI workflow. What they could not prove — what provenance structurally cannot prove — is that the source commit which triggered that workflow was legitimate.
05 — The 2026 CadenceSeven campaigns in twelve weeks.
AsyncAPI did not happen in a vacuum. Palo Alto Networks' Unit 42 documents at least six major npm mass-poisoning campaigns in 2026 before this one, each sharing the same mechanism: the attacker gains write or publish access, then rides a legitimate CI pipeline to a trusted release. The table below lists the dated campaigns from Unit 42's tracker plus July's two incidents — the same pattern we examined when a supply-chain compromise hit AI infrastructure tooling earlier this year.
| Date (2026) | Campaign | Reported scale | Gap since previous |
|---|---|---|---|
| Apr 22 | Bitwarden CLI / Checkmarx distribution | Not broken out in Unit 42's summary | — |
| Apr 29 | SAP CAP ecosystem | ~570,000 weekly downloads affected | 7 days |
| May 11 | TanStack | 373 malicious versions across 169 packages | 12 days |
| May 19 | @antv wave | 639 malicious packages published in about an hour | 8 days |
| Jun 1 | Red Hat | 32 compromised packages | 13 days |
| Jul 11 | Jscrambler | ~15,800 weekly downloads · install-hook infostealer | 40 days |
| Jul 14 | AsyncAPI | 2.66M weekly downloads (specs alone) · import-time payload | 3 days |
Read the vector column of history, not just the dates. Jscrambler (July 11) used a classic preinstall-hook native binary — the attack class npm 12 now blocks by default. AsyncAPI (July 14) used import-time execution — the class it does not. Three days apart, the ecosystem got a controlled experiment in defense lag: the same week that one vector became harder, the adjacent vector was exploited at well over a hundred times Jscrambler's download volume. The Jscrambler case stings for a second reason — the victim is itself a security and obfuscation vendor, and its plugin ecosystem was affected across multiple versions.
Projecting forward, the honest expectation is that import-time and runtime payloads become the default rather than the exception, because that is where the defensive tooling is thinnest. Registry operators can scan for lifecycle hooks cheaply; distinguishing a malicious module-load statement from the millions of legitimate side effects that fire on import is a materially harder problem. Expect the next platform-level battleground to be behavioral scanning at publish time and sandboxed module execution — and expect attackers to probe whatever those defaults leave open, on roughly the same one-news-cycle lag this incident demonstrated.
06 — Defense MatrixWould your control have stopped this attack?
Most coverage explains either what happened or what to do, in isolation. The more useful exercise is mapping each common control against the three stages of this specific chain — stage 1, the import-time spawn; stage 2, the IPFS payload fetch; stage 3, the Miasma C2 and credential theft. The verdicts below synthesize the Microsoft and Socket.dev technical breakdowns against the mitigation guidance from Unit 42, Chainguard, and StepSecurity.
| Control | Stage 1 · import spawn | Stage 2 · IPFS fetch | Stage 3 · Miasma C2 | Why / why not |
|---|---|---|---|---|
| Install-time controls | ||||
| npm install --ignore-scripts | Not stopped | Not stopped | Not stopped | There is no lifecycle script to block — the trojanized packages declare none. The payload fires when the module is imported. |
| npm 12 default install-script blocking | Not stopped | Not stopped | Not stopped | Blocks preinstall / install / postinstall hooks and implicit node-gyp builds. This campaign never used any of them. |
| Version, lockfile & publish controls | ||||
| Lockfile pinning + npm ci in CI | Stopped | Stopped | Stopped | A committed lockfile predating July 14 plus npm ci prevents semver drift onto the poisoned 6.11.2 — the chain never starts. |
| SLSA provenance / Sigstore verification | Not stopped | Not stopped | Not stopped | The malicious releases carried valid attestations — the project's authorized CI workflow really did build them. Provenance proves origin, not trustworthiness. |
| Dependency cooldown window (24-72h) | Stopped | Stopped | Stopped | Every malicious version was unpublished within roughly four hours. Even a 24-hour cooldown comfortably outlives the exposure window. |
| Runtime & network controls | ||||
| CI-runner egress allowlisting | Not stopped | Stopped | Largely stopped | The local process spawn still happens, but the IPFS gateway fetch and C2 beaconing die against a strict outbound allowlist. |
| Import-time sandboxing / isolated execution | Contained | Stopped | Stopped | Isolating what a module can spawn, read, and reach at import time limits the implant even after the poisoned code lands. |
| Credential rotation + short-lived tokens | Not stopped | Not stopped | Not stopped | Does not interrupt execution — it devalues what the stealer exfiltrates. A blast-radius control, not a prevention control. |
The pattern in the matrix is stark. Of the two controls the ecosystem spent 2025-26 celebrating — install-script blocking and provenance attestation — neither interrupts a single stage of this chain. The controls that do are older and less glamorous: a committed lockfile with npm ci, a publication cooldown, and a CI runner that simply cannot reach an IPFS gateway. Defense in depth is not a compliance phrase here; it is the only configuration in which this attack fails.
07 — The PlaybookThe layered playbook, in adoption order.
Everything below comes from the post-incident guidance of Unit 42, Chainguard, StepSecurity, and Datadog, sequenced by effort-to-payoff. The first two need no new tooling — only discipline in files you already have.
Lockfile discipline + cooldown
Commit package-lock.json, run npm ci (never npm install) in CI, and block adoption of any version published within the last 24-72 hours. Most malicious 2026 versions were pulled inside that window — AsyncAPI's in about four hours.
Kill the pwn request
Replace risky pull_request_target triggers with plain pull_request, or restrict them to explicit base-branch checkouts. Require maintainer approval before any PR-triggered workflow gains secrets access.
Egress allowlisting on runners
Limit CI outbound traffic to your private registry and known deploy targets. Alert specifically on connections to IPFS gateways and BitTorrent DHT bootstrap nodes from build infrastructure — both are in this campaign's chain.
Assume-breach credential hygiene
Short-lived tokens, scoped publish rights, and rehearsed rotation drills. This does not stop execution — it makes the 100+ credential types Miasma targets worth less when stolen.
Two additions round out the stack. First, keep verifying provenance anyway — it raises attacker cost and catches other attack classes, even though it passed cleanly here. Second, fold these layers into your pipeline design rather than bolting them on; our CI/CD pipeline design reference treats egress control and secrets scoping as first-class architecture decisions, not security afterthoughts. For teams without a dedicated platform function, this is exactly the kind of hardening our web development engagements build into delivery pipelines by default.
Note also npm's parallel identity-hardening track: alongside the npm 12 script defaults, granular access tokens configured to bypass 2FA are deprecated, with restrictions beginning August 2026 and further publishing limits announced for January 2027. That timeline is worth building into your token inventory now rather than during a forced migration.
08 — If You're ExposedThe response checklist for the unlucky four hours.
If any environment installed one of the five malicious versions during the July 14 window and subsequently imported the module, treat the machine as compromised. In order:
- Pin and reinstall clean versions. Purge node_modules and lockfile entries referencing
@asyncapi/specs6.11.2 / 6.11.2-alpha.1,generator3.3.1,generator-components0.7.1, orgenerator-helpers1.1.1. - Hunt the stage-2 artifact. Look for an unexpected NodeJS-named state directory containing
sync.jsin %LOCALAPPDATA% (Windows), ~/Library/Application Support (macOS), or ~/.local/share (Linux). - Check persistence. Microsoft documents a Windows HKCU Run key named miasma-monitor, a Linux systemd user unit (miasma-monitor.service), and macOS shell-RC injection in .zshrc, .bashrc, or .bash_profile.
- Rotate everything the stealer targets. GitHub, npm, AWS, Azure, GCP, Docker, and Kubernetes tokens; .npmrc and .aws/credentials; SSH keys; browser-saved passwords and session cookies. StepSecurity additionally recommends treating local AI coding assistant sessions and config on affected machines as compromised.
09 — ConclusionDefense that assumes the next vector.
Install-script blocking solved last year's attack. This was this year's.
The AsyncAPI compromise is the cleanest natural experiment supply-chain security has produced: six days after npm turned off the ecosystem's favorite attack vector by default, an attacker shipped 2.66 million weekly downloads' worth of malware through the vector the defaults do not touch. Nothing about the response — unpublish in roughly four hours, vendor analyses within a day — was slow. The gap was structural, not operational.
The durable lesson sits in the defense matrix: the controls that would have interrupted this chain are boring, layered, and mostly free. A committed lockfile with npm ci, a one-day cooldown on new versions, an egress allowlist on CI runners, and credential hygiene that assumes eventual breach. None of them is novel. All of them were rare enough in practice that this campaign was worth an attacker's effort.
The forward bet is equally plain: as install-time doors close, import-time and runtime payloads become the default attacker move, and the pwn-request pattern — riding a legitimate pipeline to a legitimately signed release — keeps beating any control that only verifies where code came from. Build for the vector after this one: constrain what your dependencies can do when they run, not just how they arrive.