Skip to content
Web3 release pipeline audit checklist for npm provenance, lifecycle hooks, cache poisoning, and deployer-key exposure
researchMay 19, 20263 min read

Audit the Release Pipeline Like a Smart Contract

Dmitry Serdyuk
Dmitry SerdyukCo-Founder & CDO

Updated on May 19, 2026

On May 11, 2026, between 19:20 and 19:26 UTC, attackers published 84 malicious versions across 42 @tanstack/* packages to npm. Every one of them carried valid SLSA Build L3 provenance, signed by Sigstore Fulcio, with an honest Rekor transparency log entry. By every cryptographic signal npm install cares about, they looked exactly like legitimate releases.

They were also malware.

This is the first publicly documented case of malicious npm packages shipping with valid SLSA attestations, and it broke a load-bearing assumption a lot of teams quietly held: that a green provenance badge means "this came from a controlled pipeline, so it's safe." It does not. The Mini Shai-Hulud worm — which also hit Mistral AI, Guardrails AI, and UiPath the same day — proved that a compromised pipeline produces attestations indistinguishable from honest ones. Because, in a sense, they are honest. The workflow that built the packages was the real workflow, running on the real repo, signed by the real OIDC identity. It just happened to be doing what the attacker wanted at that moment.

For Web3 teams, the implication is direct and ugly. You spend six figures auditing a 400-line Solidity contract, then ship the frontend, the deployer scripts, and the indexer through a release pipeline nobody has ever audited. That pipeline holds publish tokens, build secrets, deployer keys, and the ability to push code that millions of users execute inside their wallets. By any reasonable threat model, it deserves the same scrutiny as the onlyOwner functions of a treasury contract. It almost never gets it.

This post is the audit checklist that closes the gap, before the next worm propagates into your pipeline.

TL;DR

  • npm provenance is not a supply-chain control. It attests which CI pipeline built the artifact, not whether the pipeline was doing what you authorized. Mini Shai-Hulud shipped 84 malicious @tanstack/* versions with valid SLSA L3 attestations on May 11, 2026.
  • The attack chain reuses old primitives. A poisoned Actions cache restored into a release workflow, OIDC tokens dumped from /proc/<pid>/mem, and the npm OIDC token-exchange endpoint did the rest. None are novel — they're rarely audited together. preinstall payloads fire before installs fail; pnpm 11 moved to an allowBuilds allowlist, npm still defaults permissive.
  • Deployer keys are the on-chain blast radius. If process.env.PRIVATE_KEY exists on a CI runner that also installs npm dependencies, every transitive package in your lockfile is a deployer.
  • Audit it like you'd audit a contract. The release pipeline holds publish tokens, build secrets, and the ability to change what users sign. Treat pull_request_target like onlyOwner, Actions cache like a shared storage slot, and postinstall like initialize().

How the worm actually got in

The chain, reconstructed from TanStack's postmortem, Wiz, Socket, Snyk, and StepSecurity:

  1. The attacker forked TanStack's repo, renamed the fork to zblgg/configuration to dodge fork-list searches, and opened a pull request.
  2. That PR triggered bundle-size.yml, a pull_request_target workflow. Because pull_request_target runs in the base repo's context, the job had write access to the repo's Actions cache scope.
  3. The PR poisoned the pnpm-store cache entry that the release workflow would later restore.
  4. When a release workflow on main ran, it restored the poisoned cache and executed the attacker's binaries. Those binaries read OIDC tokens out of the Runner.Worker process via /proc/<pid>/mem.
  5. The attacker exchanged the stolen id-token: write token at npm's OIDC token-exchange endpoint (registry.npmjs.org/-/npm/v1/oidc/token/exchange/package/{package}) for a real publish token, and published 84 malicious @tanstack/* versions between 19:20 and 19:26 UTC.
  6. Because the publish flow was the real flow, the resulting packages shipped with valid SLSA Build L3 provenance signed by Sigstore Fulcio. The Rekor transparency log entry was honest. The attestation was honest. The package was malware.

Total reach as documented by Wiz and The Hacker News: 170+ compromised packages across npm and PyPI, roughly 518 million cumulative downloads in the affected version graph. @tanstack/react-router and @tanstack/react-query alone are direct or transitive dependencies of a meaningful share of the modern dapp frontend stack.

Nothing in that chain required a zero-day. Every primitive was published research: Adnan Khan's GitHub Actions cache poisoning writeup is from May 2024. pull_request_target "pwn requests" have been documented since 2021. /proc/<pid>/mem token extraction is older still. The novelty was assembling them into a release that signed itself.

Why npm provenance is not a supply-chain control

npm provenance binds a published tarball to: the source repo URI, the commit SHA, the workflow file path, and a set of build-environment claims pulled from the GitHub Actions OIDC token. That is genuinely useful. It rules out a category of attacks: "an attacker published react@99.0.0 from their laptop using a stolen NPM_TOKEN." Mini Shai-Hulud is not that category.

The SLSA framework — which npm provenance implements at Build L3 — is honest about its scope. v1.0 dropped the Source track entirely and now covers only the Build track; the SLSA v1.0 FAQ makes the limit explicit, noting that techniques like reproducible builds "do not address source, dependency, or distribution threats." And from the v1.0 Levels page, even Build L3 only promises that "tampering during the build" is prevented — not that the build was authorized, not that the source was trustworthy, not that the workflow was behaving as intended.

StepSecurity puts it the cleanest: "SLSA provenance confirms which pipeline produced the artifact, not whether the pipeline was behaving as intended." That is the right way to read every provenance badge you see in 2026.

The smart-contract analogy lands directly. A signed deployment transaction proves which key deployed which bytecode. It does not prove the bytecode is benign, that the deployer was authorized, or that the multisig signers understood what they were signing. We learned that lesson in DeFi the hard way — Bybit lost ~$1.4 billion in February 2025 because the Safe multisig faithfully signed a delegatecall that nobody on the operator side understood, after a compromised Safe developer machine swapped the UI's transaction payload.

A valid SLSA attestation on a malicious package is the npm-ecosystem version of that signed delegatecall. The cryptography is fine. The semantics are not.

Things npm provenance does NOT defend against:

Attack classWhy provenance doesn't help
Maintainer account takeoverThe legitimate account publishes via the legitimate workflow; the attestation signs the compromise.
pull_request_target "pwn requests"Fork PR executes in base-repo context; provenance correctly attests the resulting build.
OIDC token theft from a runnerThe token is exchanged for a real publish credential; provenance signs a real release.
GitHub Actions cache poisoningRestored cache is treated as trusted input; provenance signs whatever the workflow assembled.
Lifecycle scripts (preinstall / install / postinstall) on consumersProvenance is about how the tarball was built, not what its install scripts do on your machine.
Compromised transitive dependencySLSA does not propagate to transitive deps — by design.
Typosquatting / slopsquattingThe squat can itself ship valid provenance from its own legitimate-looking repo.

If your supply-chain story ends at "we only install packages with provenance," your supply-chain story ends one layer above where Mini Shai-Hulud operated.

The four chains that turn a pipeline into a wallet drain

Mini Shai-Hulud is one well-documented instance of a more general pattern. The categories in a Web3 release pipeline that actually decide whether a poisoned dependency becomes a treasury drain fall into four chains, each ending in a different way the contract layer gets bypassed.

Chain 1: from a fork PR to npm publish

This is the Mini Shai-Hulud chain. The attacker never touched the release workflow directly. They didn't have to.

The entry was a pull_request_target trigger on bundle-size.yml — a workflow that runs in the base repo's context with write access to the Actions cache scope, which is exactly the surface an attacker wants. The PR poisoned a pnpm-store cache entry. The next release run on main restored it as trusted input. By the time the attacker's binaries were executing inside the release pipeline, every privilege boundary that mattered had already been crossed.

This chain audits cleanly against a small set of questions. Treat pull_request_target like onlyOwner — any job using it is privileged and must not execute fork-supplied code, full stop. Treat Actions cache restores like network-fetched binaries; the default actions/cache flow does not verify cache contents, so cache scopes that span low- and high-privilege workflows are a confused-deputy bug waiting for a worm. And move publish off long-lived secrets: npm trusted publishing went GA on July 31, 2025, and the OIDC subject claim should pin repo + workflow file + branch + environment. A wide-open repo:org/*:* claim is the npm equivalent of tx.origin == owner.

The hard part isn't knowing these. It's that they only fail together. Each one in isolation looks defensible — which is precisely why nobody catches them. Adnan Khan's Cacheract PoC and his $31,337 Angular compromise are both worth reading before you do this audit pass; they show the assembly under conditions that matched a major OSS team's actual setup.

Chain 2: from pnpm install to deployer key

Every npm install is eval on your machine, executed with whatever privileges the runner has at that moment. That has been true since 2017. The Web3-specific question is: what privileges does that runner have?

A bad answer, still common in 2026:

- name: Deploy contracts
  run: npx hardhat run scripts/deploy.ts --network mainnet
  env:
    PRIVATE_KEY: ${{ secrets.MAINNET_PRIVATE_KEY }}

If that workflow runs pnpm install before the deploy step — and it almost always does — PRIVATE_KEY lives in the same process tree as every postinstall hook in the transitive dependency graph. A worm that lands one package in the lockfile owns the deployer. Shai-Hulud v2 switched its payload from postinstall to preinstall specifically so the code fires before the install can fail. Aborting on an error does not save you.

The fix is two moves, both well-supported. First, get the key out of env. Hardhat 3's Configuration Variables and hardhat-keystore replace plaintext env vars; Foundry supports --ledger, --trezor, --aws KMS, --keystore, and --interactive directly. Fireblocks ships a Hardhat plugin. For anything privileged, MPC or KMS-backed signing means the key material never lands in runner memory at all.

Second, change what the deployer can do once it has signed. The EOA that calls forge create should hold no admin, upgrade, or pause rights. Use a low-privilege deterministic deployer — the Arachnid CREATE2 factory is the convention — and transfer ownership to a Safe multisig behind a timelock in the same broadcast. A compromised deployer that can only schedule an upgrade buys defenders a recovery window. A deployer that can upgradeTo() immediately does not. The same root-on-CI-runner outcome is exactly what we covered in Copy Fail — a kernel privilege escalation, a poisoned package, an exposed deploy key, same blast radius.

A separate defense lives on the install side. pnpm 11 ships with minimumReleaseAge defaulting to 1440 minutes — a 24-hour quarantine that catches most public worm cycles before they reach pnpm install. The same release moved dependency lifecycle scripts behind an explicit allowBuilds allowlist. If you're on npm or Yarn classic, you're choosing the permissive default; ignore-scripts=true in .npmrc plus a manual rebuild list for the packages you actually need to compile is the closest equivalent.

Chain 3: from frontend bundle to wallet drain

The contract layer is bypassed entirely if an attacker can change what users sign. This is the dominant pattern in the largest crypto losses of the last two years.

Bybit lost ~$1.4B in February 2025 because a Safe developer's macOS workstation was compromised, malicious JavaScript was injected into Safe's S3-hosted UI, and the multisig signers approved a delegatecall that nobody on the operator side understood. The contract was fine. The multisig was fine. The frontend was the attack.

The npm-specific case is sharper. Ledger Connect Kit, December 2023: a phished ex-employee's npm credentials were never revoked; a malicious version was published to npm; and SushiSwap, Kyber, and Revoke.cash all loaded a wallet drainer for several hours before anyone noticed. That is the exact chain Mini Shai-Hulud points at — compromise the publisher, ship through the legitimate distribution path, hit every dapp that pinned the dependency loosely. (Radiant Capital in October 2024 and the Curve / Squarespace DNS hijacks are the same chain through different doors — dev-machine malware, DNS provider compromise.)

The mitigations are unsexy and well-known. Hash and sign frontend bundles before they reach the CDN; verify on deploy. Do not serve transitive npm packages from production — the build runs in CI, production serves immutable artifacts. Treat DNS like the security boundary it is: DNSSEC, registrar lock, MFA that survives migration. Offboard contractors from every credential surface, npm included.

EIP-7702 raises the stakes. Preprint research from late 2025 tracks a cluster of malicious sweeper contracts ("CrimeEnjoyor") being used as delegation targets for compromised EOAs at meaningful scale. A compromised frontend that can convince a user to sign one 7702 authorization converts the EOA itself into a persistent attacker proxy — every future transaction routes through attacker logic. The old "drain one approval at a time" model was bad. This one is structurally worse.

Chain 4: from a leaked token to a quiet pivot

The fourth chain is the one that doesn't show up in a postmortem until weeks later, because the attacker took the long way around.

A token leaks. Maybe it's a GITHUB_TOKEN with broader scope than its workflow needs — Grafana's May 17 incident is the latest in that lineage. Maybe it's an NPM_TOKEN an offboarded contractor never had revoked. Maybe it's an OIDC subject claim wildcarded across the org so any workflow can publish anything. The attacker doesn't smash and grab. They pivot — adjacent repos, dependent packages, alerting and webhook secrets — and they do it quietly.

The defenses here are mostly process. Maintain an inventory of every CI secret and which workflows can read it; you cannot revoke what you have not enumerated. Minimize scope ruthlessly — a workflow that only needs contents: read should not have id-token: write. Prefer OIDC over PATs everywhere it's available; short-lived tokens with auditable subject claims beat long-lived secrets every time. And the part most teams skip: write the revocation runbook, then rehearse it. When a runner is suspected compromised, the order of operations decides the blast radius — cloud creds first, then GitHub tokens, then npm publish, then deployer keys, then user-facing communications. The Delve / fake SOC 2 case is what happens when the runbook doesn't exist; The Human Factor covers why the people-side of the runbook is usually the part that breaks.

Runtime detection is the catch-net for when one of the four chains gets ahead of the patch cycle. Alert on npm and pnpm install hooks spawning shells, curl, wget, Bun, GitHub API calls, the cloud metadata IP (169.254.169.254), or writes to systemd user services. Alert on unexpected reads of /proc/<pid>/mem or /proc/<pid>/environ on CI runners — the OIDC-extraction primitive from Mini Shai-Hulud. Alert on publish events from any workflow other than the designated release workflow. On-chain: admin role changes, unscheduled upgradeTo(), deployer-key transactions outside known release windows, multisig signer or threshold changes. DNS: NS record changes, registrar logins from new IPs.

Audit it like a contract

The argument compresses into a single mapping. Treat the release pipeline as a privileged contract; the abstractions you already use for onlyOwner, delegatecall, storage collisions, and timelock translate directly.

Smart contract conceptRelease pipeline equivalent
onlyOwner modifierBranch protection, required reviewers, CODEOWNERS, restricted release workflows
delegatecall to untrusted codepull_request_target workflows, third-party Actions pinned by tag
Storage slot collisionsActions cache scope shared across workflows of different privilege
Initialization functionpreinstall / install / postinstall lifecycle hooks
TimelockminimumReleaseAge quarantine, deployer-side timelocks on admin paths
Upgrade pathnpm publish, container push, frontend bundle to CDN
External oracleThird-party Actions, base images, dependency mirrors

Run the questions you'd ask of a TimelockController deployment against your release workflows. Most teams find at least two delegatecall-equivalents on the first pass. The rest of the audit follows from there.

FAQ

What is npm provenance and what does it actually prove?

npm provenance is a SLSA-format attestation bound to a published package tarball. It proves which source repository, commit SHA, workflow file, and GitHub Actions OIDC identity built the package. It does not prove the workflow was authorized to run, that the build was uncompromised, or that the code is benign — only that the build happened in the named pipeline.

Did Mini Shai-Hulud packages have valid SLSA provenance?

Yes. According to Snyk and StepSecurity, the 84 malicious @tanstack/* versions published on May 11, 2026 shipped with valid SLSA Build L3 attestations signed by Sigstore Fulcio, with Rekor transparency log entries. The provenance was honest — the workflow was compromised, but it was the real workflow running on the real repo, so the attestation correctly described what happened.

How did the attacker extract publish credentials from the GitHub Actions runner?

The chain: a forked PR triggered a pull_request_target workflow that poisoned the pnpm-store cache. A subsequent release workflow on main restored the poisoned cache and executed attacker binaries. Those binaries read OIDC tokens out of the Runner.Worker process via /proc/<pid>/mem, then exchanged the tokens at npm's OIDC endpoint (registry.npmjs.org/-/npm/v1/oidc/token/exchange/package/{package}) for real publish tokens. (Wiz analysis, TanStack postmortem.)

Should Web3 teams set --ignore-scripts by default?

If you can run pnpm v10+ with allowBuilds, that's a cleaner default — you get explicit allowlisting of which packages may execute install scripts, without the global breakage of --ignore-scripts. If you're stuck on npm or Yarn classic, ignore-scripts=true in .npmrc plus an explicit list of packages you npm rebuild after install is the next-best pattern. Either way, install steps should not run on hosts that hold publish tokens or deployer keys.

What's the single most important Web3-specific change?

Move deployer keys out of CI env vars. A Foundry deploy with --ledger or --aws KMS, plus an admin handoff to a Safe multisig behind a timelock in the same transaction, removes the worst outcome — a compromised npm dependency immediately owning your protocol's upgrade path. The Bybit and Radiant incidents are both cases where every other control was in place and the human-trusted signing path was the bypass.

How does this connect to the Bybit and Radiant incidents?

Different vector, same shape. Bybit ($1.4B, Feb 2025) and Radiant ($50M, Oct 2024) both compromised a developer workstation, not a contract. Mini Shai-Hulud generalizes that pattern to "compromise the build pipeline at any point and the rest of the on-chain controls don't help." The audit boundary needs to extend up the supply chain, not just down into the contract code.

Where SigIntZero fits

A useful protocol review does not stop at the contract boundary. The release pipeline, signing infrastructure, frontend release path, and on-chain monitoring of admin paths are part of the same security perimeter.

  • Sentinel — AI-assisted smart contract auditing that documents the deployment, upgrade, and operator assumptions a contract relies on. The audit covers the boundary between contract and infrastructure, including release-pipeline assumptions.
  • Tripwire — runtime monitoring of on-chain symptoms: admin role changes, unscheduled upgrades, deployer-key usage, governance anomalies, oracle drift, bridge message inconsistencies. Tripwire cannot prevent a poisoned cache. It can detect the on-chain consequences of one.
  • Services — protocol security review beyond the contracts: CI/CD, key management, frontend release, incident response.

If your protocol's perimeter includes a release pipeline that can publish, deploy, sign, or change what users sign, that pipeline is part of your audit scope. Talk to us if you want a second set of eyes on it.

The lesson outlasts the worm

Mini Shai-Hulud will be cleaned up. The compromised @tanstack/* versions will be unpublished, the OIDC tokens rotated, the cache-poisoning primitive papered over with another GitHub mitigation. The underlying lesson will not change.

Provenance proves who built a package, not whether the build was honest. Smart-contract audits prove the contract is correct, not that the deploy was authorized. Both are necessary. Neither is sufficient. The release pipeline sits at the seam between them, holding the credentials that decide both questions in production. Audit it on purpose, before something else does it for you.


Sources and further reading:

Dmitry Serdyuk
Dmitry Serdyuk

Co-Founder & CDO

Full-Stack Operator | Building across security, AI, and digital infrastructure.