Javlon Baxtiyorov
← Writing

The signature was valid. That was never the question.

AsyncAPI shipped OIDC trusted publishing, Sigstore signing, and SLSA provenance — and an attacker still pushed five trojanized packages to npm through the project's own legitimate pipeline. The break was 65 lines of YAML that ran before signing ever happened.

The signature was valid. That was never the question.
Photo · Unsplash

On July 14, 2026, five packages in the AsyncAPI npm namespace shipped with a credential stealer inside them, and every one of them carried a valid SLSA provenance attestation. The signatures were correct. The build platform was the real build platform. The commit-to-artifact chain checked out. AsyncAPI had done what the entire supply-chain-security industry has spent two years telling maintainers to do — OIDC trusted publishing, Sigstore signing, SLSA attestation — and it made no difference at all, because the attacker never touched the signing step. The target was a workflow that decided what got to run before signing happened.

A fix for the exact hole had been open for 58 days.

What happened, in one paragraph

Between 05:03 and 06:14 UTC, someone opened around 36 near-identical decoy pull requests against asyncapi/generator — all variations on docs: add donation files — to bury one real PR, #2155, in noise. PR #2155 triggered the Docs Preview (Netlify) workflow (run 29307859879), which ran on the pull_request_target event but checked out the pull request's untrusted code. That combination handed the attacker's code a context with real secrets in it. By 05:17 UTC the injected code had scraped an asyncapi-bot GitHub PAT and a Netlify token and posted them to a Rentry.co paste. From 05:40 to 07:05 UTC the attacker used that PAT to push — and force-push — to the update and next branches. The push to next auto-triggered the project's real release pipeline. By 08:30:09 UTC, that legitimate pipeline had published five trojanized versions to npm. Microsoft telemetry logged the first downstream fetch at 08:49:22 UTC, about 43 minutes later.

No npm token was ever stolen. That detail is the whole story.

pull_request_target is still a loaded gun

GitHub gives you two events for PR automation. pull_request runs with the untrusted fork's code but a read-only, secret-less token — safe, and limited. pull_request_target runs in the context of the base repository, with full secret access, so it can post preview links, label PRs, or leave review comments. The rule that makes it safe is simple: never check out or execute the PR's code in that context. AsyncAPI's Netlify preview workflow did exactly that. It checked out the untrusted head of the fork and then ran it, with the base repo's secrets sitting in the environment.

That is not a novel bug. It is the same misconfiguration behind the March 2025 tj-actions/changed-files compromise, which leaked secrets from more than 23,000 repositories. The vulnerability class has a name in GitHub's own security guidance and a standing warning in the Actions docs. It has been written up, exploited, and re-exploited.

The part that should sit with anyone running CI: a contributor filed a working proof-of-concept for this specific pull_request_target hole in AsyncAPI on April 29, 2026. The fix sat unmerged for 58 days. The exploit didn't require research. It required someone to read an open issue.

What the attacker actually stole

The naive mental model of an npm supply-chain attack is "they stole the npm publish token." That model is why so much defensive effort went into trusted publishing — kill the long-lived token, replace it with short-lived OIDC, and there's nothing left to steal. It's good advice. It solves a real problem. It did nothing here.

What the attacker took was a GitHub PAT and a Netlify token. The injected payload scanned /proc/*/environ across running processes for INPUT_GITHUB-TOKEN and NETLIFY_AUTH_TOKEN, and patched the checkout action's dist/index.js to exfiltrate tokens after it ran. With the PAT they didn't publish anything themselves. They pushed malicious commits to a release branch and let AsyncAPI's own pipeline do the publishing — OIDC handshake, Sigstore signature, provenance attestation, all of it, faithfully, on the attacker's code. The identity that published was the real identity. It was just being puppeted.

The payload itself is worth naming for anyone hunting it: it self-identifies internally as M-RED-TEAM v6.4 under the campaign tag miasma-train-p1, and it fires at import/require time rather than in a postinstall hook. That is a deliberate choice. npm install --ignore-scripts and most CI hardening built for the postinstall era never see it — the code runs the moment something requires the package. Microsoft Defender flags it as Trojan:JS/MiasmStealer.SC / Trojan:Script/Supychain.A.

Why the provenance was valid — and why that's the point

SLSA provenance answers one question honestly: what happened during the build. Which platform built the artifact, from which workflow, tied to which source commit. Every one of those assertions was true for the trojanized packages. The build platform was AsyncAPI's real platform. The workflow was AsyncAPI's real release workflow. The commit was a real commit on the next branch. The attestation didn't lie. It recorded a legitimate build of a malicious commit, and it recorded it accurately.

SLSA says this about itself, in plain words: a signed artifact is not necessarily a trustworthy one. Provenance tells you what happened. It does not tell you whether what happened was authorized. Those are different questions, and the second one is the one that mattered on July 14.

The gap these tools were never built to close

It helps to separate three things that get collapsed into "supply-chain security." There is identity — who published this (OIDC trusted publishing). There is build integrity — was the artifact built the way the provenance claims (SLSA, Sigstore). And there is source integrity — was the commit that triggered the build authorized to be there. The AsyncAPI attack passed the first two cleanly and failed the third completely. The attacker held a legitimate identity and produced a legitimate build; the input to that build was a commit they had no right to push.

No signature can close a source-integrity gap, because by the time the signing step runs, the malicious commit is already the source of truth. That gap is not a place for more cryptography. It's a place for policy and human review — the parts of the system that decide whether a triggering commit should be trusted before the pipeline picks it up.

What would actually have stopped this

None of the fixes are exotic. Every one is something a team can check today.

  • Branch protection on any branch that auto-publishes. The next push triggered a release. A branch that can ship to npm on push should not accept a direct push from a bot PAT with no review.
  • Split the pull_request_target job. Do the privileged work (posting the preview link) in the trusted context with no checkout of fork code; do any build of untrusted code in a separate, secret-less pull_request job. Never let the two share an environment.
  • Environment-scoped secrets. The Netlify token and bot PAT were reachable from a preview workflow. Scope publish credentials to the environments that actually publish, so a docs-preview job can't see them.
  • SLSA Build L3 platforms with real isolation, so a build can't reach into sibling process memory the way this payload did through /proc.
  • A human review gate on release triggers. A push to a release branch that results in a public npm publish is exactly the kind of irreversible, outward-facing action that should require a person in the loop.

For teams running CI publishing pipelines

If you've rolled out npm trusted publishing and SLSA provenance, keep them — they raise the floor and they close real attack paths. Just be honest about what they close. They prove your build happened the way you said it did. They do not vet the commit that started it.

So the short version, the part worth pinning to the wall:

  1. Audit every pull_request_target workflow for a checkout of PR code. That single pattern is the recurring root cause.
  2. Put branch protection and a review gate on any branch that can publish.
  3. Scope secrets to environments; assume any workflow that can read a token will eventually leak it.
  4. Treat import-time payloads as the norm now — --ignore-scripts is not the control it used to be.

If you run AsyncAPI packages, the affected and safe versions are documented by the responders (Chainguard, Datadog, Wiz, StepSecurity) along with the full IOC and remediation detail — pin to the safe releases and rotate anything the compromised versions could have touched, rather than trusting a checklist in a blog post to be complete. The compromised range is narrow and the safe versions are one patch below each.

The uncomfortable takeaway is not that AsyncAPI was careless. They ran more supply-chain tooling than most projects their size. The attack succeeded anyway because the tooling was pointed at the signing step, and the door was 65 lines of YAML upstream of it. Provenance did its job perfectly. It signed the crime scene.

Read next All writing →
← All writing Get in touch →