Supply Chain Active Exploit GitHub Actions

One pull request backdoored four AsyncAPI packages with a valid seal

July 17, 2026·9 min read·Chris Boker, Founder, CVEasy AI
An attacker's pull request card on the left feeds into a rust workflow gear that leaks the asyncapi-bot personal access token as a gold key, the key flows across the frame into a signed tarball on the right stamped with a bold gold SLSA seal, and beneath the tarball a rust strip highlights the first physical line of index.js where the padded loader hides

Any CI pipeline that ran npm install @asyncapi/generator between 07:10 and 11:18 UTC on July 14, 2026 pulled a tarball whose SLSA attestation was cryptographically valid, whose maintainer was correct, whose GitHub Actions workflow was the one you would expect, and whose index.js began with a line of whitespace followed by a spawned child process reading environment variables. Four packages were affected: @asyncapi/specs, @asyncapi/generator, @asyncapi/generator-components, and @asyncapi/generator-helpers, together downloaded more than three million times a week (BleepingComputer). Nothing was signed with an attacker key. The legitimate release pipeline signed everything for them.

The pwn request that walked out with asyncapi-bot

The AsyncAPI generator repository had a workflow called Docs Preview (Netlify) that fires on pull_request_target whenever a PR touches apps/generator/docs/** (Datadog Security Labs). That trigger runs in the base repository's context with the base repository's secrets, and it does not gate on maintainer approval by default. GitHub introduced it so preview builds could comment on a fork PR without a per-PR secret dance. The moment the workflow checks out the PR's head commit rather than the base commit, the attacker's code runs with the base repo's tokens attached.

That is what happened. The attacker opened a PR with a benign-looking docs change, the workflow fired, and the checkout step pointed at attacker code. The payload patched the runtime file the runner had already staged at /home/runner/work/_actions/actions/checkout/v5/dist/index.js so the exfil executed on the action's exit path, by which point the runner environment held INPUT_GITHUB-TOKEN, NETLIFY_AUTH_TOKEN, NETLIFY_SITE_ID, and the asyncapi-bot PAT the release automation used (Datadog Security Labs). It also scraped /proc/*/environ for anything else the pipeline had loaded and posted the set to a paste service.

Once the bot PAT was out of the runner, the attacker had push rights on protected branches and could trigger release workflows exactly as the maintainers would.

The release pipeline signed everything on the way out

Between 07:10 and 11:18 UTC on July 14, five versions were cut from those branches: @asyncapi/specs 6.11.2-alpha.1 and @asyncapi/specs 6.11.2, @asyncapi/generator 3.3.1, @asyncapi/generator-components 0.7.1, and @asyncapi/generator-helpers 1.1.1 (The Hacker News). Each shipped through npm's trusted publishing flow, which reads a GitHub OIDC token from the release workflow and hands npm a signed statement about who built the tarball and where. That statement was accurate: the workflow, the repository, and the commit hash were all the real ones. It was just also a commit an attacker had pushed under the bot's identity.

Microsoft put it crisply: the attestations "accurately identified the legitimate repositories, commits, and workflows that created the packages, even though the triggering commits were unauthorized" (Microsoft Security Blog). SLSA provenance answers who built the tarball and where. It does not answer whether the commit was authored by anyone authorized to push. That gap is the whole attack.

The core problem: a valid SLSA seal binds a tarball to a commit, workflow, and repository. It does not attest that the commit was pushed by anyone authorized. When the release bot is one PAT leak away, trusted publishing signs the attacker's work with the same key it signs the maintainer's.

The payload runs when your code imports the module, not when npm installs it

The community-taught defense is npm install --ignore-scripts, which suppresses preinstall and postinstall hooks. This campaign moved earlier in the lifecycle. The loader was pasted onto the first physical line of the primary entry file for each package, padded with whitespace so an editor scrolled past it. The targets were index.js in @asyncapi/specs, lib/templates/config/validator.js in @asyncapi/generator, src/utils.js in @asyncapi/generator-helpers, and lib/utils/ErrorHandling.js in @asyncapi/generator-components (Microsoft Security Blog).

Any require() or import of those files ran the loader at build time or server startup. It spawned a detached Node process that pulled a second stage of roughly 8.2 MB from a hard-coded IPFS content identifier, wrote it into an OS-specific cache directory, and handed it control. The second stage is the Miasma descendant analysts have been tracking through several npm and PyPI operations this year: elliptic-curve command validation with secp256k1, C2 fallbacks over Nostr and libp2p rather than plain HTTPS, and a credential harvester that reads ~/.npmrc, ~/.aws/credentials, ~/.kube/config, SSH keys, .vault-token, .netrc, and .docker/config.json, plus more than a hundred named tokens in the process environment (Microsoft Security Blog).

For CI runners the exposure is simple: anything the pipeline held in memory when require('@asyncapi/specs') first executed is out of the runner, including deploy tokens, cloud IAM material, and workflow secrets.

Why a CVE-centric queue reads this as background noise

There is no CVE for the AsyncAPI packages and no NVD entry. A scanner that indexes CVE feeds against installed dependencies sees @asyncapi/generator 3.3.1, matches nothing, and returns clean. The provenance attestation says legitimate, so any SLSA verifier passes. The package name did not change, the maintainer did not change, the publisher account did not change, and the download counts kept climbing.

That is the pattern the last twelve months of npm and PyPI compromises have hardened into. The affected component is the identity of a build, not the version of a library. CVSS has nothing to score. KEV cannot list what does not have a CVE. A queue built on those inputs is not wrong, it is asking a question the attack does not answer.

Four TRIS layers that decide this queue

TRIS is the Threat and Risk Intelligence Scoring engine inside CVEasy AI. Four layers land this event where it belongs.

Exploitation state. Confirmed. Five malicious versions were live on npm for roughly four hours and lodged themselves in install caches before npm unpublished them. Microsoft, Datadog, StepSecurity, Wiz, Chainguard, and Socket reproduced the payload and its second stage in analysis environments (Wiz, Datadog). Any install in the window is treated as a successful attack, not a probability.

Blast radius from your build boxes. TRIS reads the runner side of your inventory: which CI environments installed the tainted versions in the window, which of those held cloud IAM material, npm publish tokens, GitHub App keys, or Vault tokens with material scopes, and which build outputs were signed downstream. A laptop install is serious. A release runner that touched cloud credentials and pushed a signed artifact to customers is a full incident.

Provenance trust decay. Any tarball produced by the affected AsyncAPI branches during the window is provenance-tainted even when the attestation verifies, because the attestation says nothing about commit authorship. Any downstream artifact whose SBOM lists those inputs inherits the same tag until the build is replayed against a clean base.

Patch and rotation state. Unpublishing closed the front door only. TRIS marks the finding closed after lockfiles resolve to none of the five bad versions, every secret the affected runners could see has been rotated, any signed downstream artifact tied to the tainted branches has been rebuilt from a clean commit, and the pipeline no longer runs untrusted PR code with the release bot's PAT in scope.

Net effect: a project pinned before July 14 that never rebuilt in the window sits low on the queue. A runner that pulled @asyncapi/generator 3.3.1 at 09:00 UTC with an AWS role and an npm publish token attached lands top-band ACT with a rotation checklist attached.

What to check, what to rotate, what to pin

Search every build log, install trace, and lockfile from July 14, 2026 for any resolution of @asyncapi/specs 6.11.2-alpha.1, @asyncapi/specs 6.11.2, @asyncapi/generator 3.3.1, @asyncapi/generator-components 0.7.1, or @asyncapi/generator-helpers 1.1.1. Treat any matching runner as compromised for secret handling.

Rotate cloud IAM credentials, npm and GitHub tokens, container registry credentials, deploy keys, and any Vault or KMS material the runner could reach. Revoke SSH keys and long-lived personal access tokens that lived on developer workstations which ran the packages outside CI. Look for outbound requests to IPFS gateways from build hosts across the July 14 window, because the second stage arrived over gateway HTTP before switching to Nostr and libp2p.

Pin AsyncAPI packages to the maintainers' post-remediation releases and lock the exact commit hashes in your lockfile. Audit your own pull_request_target workflows for the same misconfiguration. Egress controls like StepSecurity's Harden-Runner would have caught the paste-site exfil during the initial workflow abuse and are cheap to enable (StepSecurity).

Rotate on the basis of exposure, not confirmation. Any runner that imported one of the five versions inside the window ran attacker code with the runner's environment in reach. If you cannot prove a specific secret was not present, rotate it. IPFS gateways and mesh channels do not leave the same telemetry HTTPS beacons do.

How CVEasy AI surfaces this

CVEasy AI, the number one local-first CTEM platform, does not wait for an NVD entry to flag a supply-chain event. When Microsoft, Datadog, StepSecurity, Chainguard, and Wiz publish a coordinated compromise report, our ingest pipeline picks up the affected packages, exposure window, and reproduced payload signatures and scores them through TRIS against your SBOM and CI inventory on your hardware. Runners that resolved a tainted version inside the window land in ACT with a rotation checklist. Lockfiles and secret inventory never leave your infrastructure.

Sources: BleepingComputer, The Hacker News, Microsoft Security Blog, Datadog Security Labs, Wiz, Chainguard, StepSecurity