CyberBytes Daily

Trending cyberattacks, explained simply.

supply chain attacks

How attackers poisoned a build pipeline's cache to publish 84 malicious packages with valid security certificates

Every one of the 84 malicious packages published in this attack carried a valid cryptographic certificate proving it was built by TanStack's official release pipeline. The certificates were not forged. They were not stolen. They were legitimately issued, because the attacker had already taken control of the pipeline before the signing step ran.

That is the core of the Mini Shai-Hulud attack, attributed with high confidence to a group called TeamPCP. By exploiting a known misconfiguration in GitHub Actions, the group poisoned a shared build cache, waited for a real TanStack developer to trigger a release, and then extracted a short-lived identity token directly from the build server's memory. In six minutes, they published malicious versions of 42 packages to npm. Every package passed the cryptographic checks that the security community has spent years building. Within 48 hours, the worm had spread to 172 packages across npm and PyPI, touching an estimated 518 million cumulative weekly downloads.

The detail that should concern every engineering leader: the attacker installed a dead-man's switch on infected developer machines. If a developer revoked their credentials before removing the malware, the malware wiped their home directory. Standard incident response, the first thing any security team tells you to do, was weaponized against the people trying to respond.

Narrative · 7 min read

The Context

TanStack is an open-source project that produces a widely used family of JavaScript libraries, including @tanstack/react-router and @tanstack/query. These tools are embedded in the front-end codebases of thousands of companies. @tanstack/react-router alone receives 12.7 million downloads per week. When a package this widely used is compromised, the blast radius is not measured in individual organizations but in the aggregate surface area of every application that runs it.

The attack was the fourth confirmed wave from a group called TeamPCP (also tracked as DeadCatx3, PCPcat, ShellForce, and CipherForce, and formally designated UNC6780 by Google's threat intelligence team). The group had been running supply chain attacks since at least March 2026, previously compromising tools from Aqua Security, Checkmarx, and SAP. The TanStack wave was their most technically ambitious operation.

The Attack, Phase by Phase

Phase 1: Poisoning the Build Cache

On May 10, 2026, the attacker created a GitHub account called voicproducoes and forked the TanStack/router repository, immediately renaming it to zblgg/configuration to prevent it from appearing in GitHub's fork-list searches. A single commit (79ac49ee) added two things: a 2.3 MB obfuscated JavaScript payload called tanstack_runner.js, and a fake package called @tanstack/setup whose install script ran the payload automatically.

The attacker then opened a pull request from this fork back to the real TanStack/router repository. This triggered a workflow called bundle-size.yml, which used the pull_request_target trigger type—a known misconfiguration in GitHub Actions that runs with base-repository permissions even when the code comes from an untrusted fork. The workflow checked out the fork's code and executed it, writing a malicious version of TanStack's dependency store into the shared GitHub Actions cache.

The attacker did not need the pull request to be merged. The trigger fired the moment the PR was opened.

ATTACKER ACTIONS: FORK AND CACHE POISONING🍴1Fork renamed to evade detectionzblgg/configuration hides from fork-list📝2Malicious commit added2.3 MB payload + fake @tanstack/setup🔀3Pull request openedTriggers pull_request_target with base permissions☠️4Cache poisonedMalicious pnpm store written to shared cacheThe attacker never needed the pull request to be merged. The trigger fired on open.

Phase 2: Extracting the Identity Token and Publishing

On May 11, 2026 at 19:15 UTC, a legitimate TanStack maintainer merged a real pull request, triggering the release workflow. That workflow restored the dependency store from the shared cache—the poisoned one.

The attacker's code, now running inside the official release workflow, read directly from the build server's process memory to extract the OIDC token GitHub had issued for that run. This token proves to npm that a request is coming from TanStack's release pipeline. TanStack's trusted-publisher configuration granted trust at the repository level, not scoped to a specific branch or workflow file, so the hijacked token was fully valid for publishing.

Between 19:20 and 19:26 UTC—six minutes—the attacker published 84 malicious versions across 42 @tanstack/* packages. Every one carried a valid SLSA Build Level 3 provenance certificate, generated by feeding the stolen OIDC token into Sigstore. The certificates correctly stated the packages were built by TanStack's release pipeline on the main branch. That was technically true. It just omitted that the pipeline had been running attacker code.

This is the first documented case of malicious npm packages carrying valid SLSA provenance attestations.

TOKEN EXTRACTION AND MALICIOUS PUBLISH🔀1Legitimate PR merged to mainRelease workflow restores poisoned cache🧠2OIDC token read from memoryAttacker reads /proc/pid/mem of runner🪙3Publish token mintedStolen OIDC token exchanged for npm credential📦484 malicious versions published42 @tanstack/* packages in six minutes5Valid SLSA certificates generatedSigstore signs each package as legitimateNo npm password or API key was ever stolen. The attacker used TanStack's own identity.

Phase 3: Credential Harvest and Worm Propagation

Once installed, the malicious packages executed their payload via npm lifecycle hooks, running under the Bun JavaScript runtime rather than Node.js—a deliberate choice to evade security tools that hook into Node.js specifically.

The payload harvested credentials from cloud providers (AWS, Google Cloud, Azure), Kubernetes service account tokens, HashiCorp Vault secrets, GitHub tokens, SSH keys, cryptocurrency wallets, and—for the first time in this campaign family—password managers including 1Password and Bitwarden. It then located npm tokens capable of bypassing two-factor authentication, enumerated every package each compromised maintainer controlled, and republished poisoned versions using exchanged OIDC tokens. The worm spread to @uipath, @mistralai, @squawk, @opensearch-project, and dozens of other namespaces within hours.

Exfiltration used three redundant channels: a typosquat domain (git-tanstack[.]com), the decentralized Session messenger network, and GitHub repositories created with stolen tokens and given Dune-themed names.

WORM PROPAGATION ACROSS THE ECOSYSTEM💻1Payload executes on installRuns via npm lifecycle hook under Bun🔑2Credentials harvestedCloud, Kubernetes, GitHub, SSH, passwords🔍3Maintainer packages enumeratedEvery package the victim controls is targeted🐛4Worm republishes poisoned versionsOIDC tokens exchanged to publish without 2FA📦@uipath (65 packages)Compromised in secondary wave📦@mistralai, @squawkSpread within hours📊172 packages total403 malicious versions in 48 hoursThe worm used three redundant exfiltration channels, including a decentralized messenger network.

Phase 4: Persistence and the Dead-Man's Switch

On infected machines, the payload wrote persistence hooks into Claude Code's session startup configuration, VS Code's folder-open task runner, a macOS LaunchAgent, and a Linux systemd user service—causing the payload to re-execute whenever the developer opened their tools.

The most consequential behavior was the dead-man's switch. A background process called gh-token-monitor polled GitHub every 60 seconds. If it detected that a stolen token had been revoked, it immediately deleted the user's entire home directory. The token was named IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner.

This directly targeted the standard incident response playbook. Security teams are trained to revoke compromised credentials immediately—but doing so before removing the malware destroyed evidence and the developer's work. The daemon automatically exited after 24 hours, giving the attacker a window to operate while forcing responders to slow down.

PERSISTENCE AND DEAD-MAN'S SWITCH🔁1Persistence written to four locationsClaude Code, VS Code, LaunchAgent, systemd👁️2gh-token-monitor daemon startsPolls GitHub every 60 seconds for token status⚠️3Token revoked by responderStandard incident response triggers the switch💥4Home directory wipedrm -rf destroys evidence and developer filesThe daemon auto-exited after 24 hours. Revoking credentials before removing the malware was the wrong move.

What Made This Possible

  1. The pull_request_target misconfiguration is widely deployed. GitHub has documented this trigger as dangerous for years. TanStack's bundle-size.yml workflow used it and checked out fork code inside that privileged context—the specific combination that enables cache poisoning.

  2. The Actions cache crosses trust boundaries. The cache is writable by fork-triggered workflows with weak permissions and readable by release workflows with strong permissions. No integrity verification exists between the write and the read, so a low-privilege workflow can inject content that a high-privilege workflow will later execute as trusted.

  3. OIDC trusted publishing was scoped too broadly. TanStack's npm trusted-publisher configuration trusted any workflow run from the repository, not just runs from a specific protected branch and workflow file. Once the attacker controlled any workflow run, the extracted OIDC token was valid for publishing.

The systemic failure: every security control TanStack had correctly implemented—SLSA provenance, OIDC trusted publishing, Sigstore attestations—was designed to answer "did this artifact come from this pipeline?" None could answer "was this pipeline running only code its maintainers authorized?"

What Should Have Stopped This

  • Restrict pull_request_target workflows. Workflows using this trigger should never check out or execute code from the triggering fork. The fix is to separate the privileged workflow from any step that touches fork code.
  • Scope OIDC trusted publishing to a specific branch and workflow file. npm's trusted-publisher configuration supports this. Granting trust only to release.yml running on refs/heads/main would have invalidated the stolen token.
  • Implement cache integrity verification. Before a release workflow restores a cached dependency store, it should verify the cache's cryptographic hash against a value stored outside the cache itself.
  • Check for the persistence daemon before revoking credentials. Search for gh-token-monitor as a macOS LaunchAgent or Linux systemd service and remove it before rotating any tokens.

The Takeaway

This attack shares the same structural failure as the Stryker Intune wipe: a trusted management system weaponized against the organization it was built to protect. The security layer verified that the right system was acting, but could not verify that the system was acting on authorized instructions. It also connects directly to the Axios build-time injection pattern: in both cases, attacker code ran during the build process, upstream of every signing and verification step, making the output of those steps a false signal of legitimacy.

Pattern to remember: Cryptographic provenance proves where code was built, not whether the build environment was running authorized code. An attacker who controls the build environment controls the certificate.

What changed: Build pipeline identity tokens can now be extracted from live process memory and used to generate cryptographically valid provenance attestations for malicious packages, meaning SLSA certificates no longer distinguish a clean build from a compromised one.

Technical Deep Dive · 4 min

The Technical Mechanism

The attack chained three distinct GitHub Actions vulnerability classes, none sufficient alone.

Vulnerability 1: pull_request_target Pwn Request (bundle-size.yml). The pull_request_target event runs in the context of the base repository with base-repository secrets and permissions, even when triggered by a fork PR. TanStack's bundle-size.yml workflow used this trigger and included a actions/checkout step that checked out ${{ github.event.pull_request.head.sha }}, the fork's code, inside the privileged context. This allowed the attacker's code to execute with base-repository write permissions, including write access to the Actions cache. This is a documented misconfiguration class, sometimes called a "Pwn Request," described in GitHub's own security hardening documentation.

Vulnerability 2: Cross-trust-boundary cache injection. GitHub Actions caches are keyed by cache key strings and are shared across workflow runs in the same repository. A fork-triggered workflow with write access to the cache can write entries that a subsequent release workflow will restore. There is no integrity verification between cache write and cache restore. The attacker poisoned the pnpm dependency store cache key used by the release workflow, replacing legitimate dependencies with a store containing tanstack_runner.js (SHA-256: ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c) and a fake @tanstack/setup package whose prepare lifecycle hook executed the payload. The trailing && exit 1 in the prepare script caused the optional dependency to fail gracefully, leaving minimal log traces.

Vulnerability 3: Overly broad OIDC trusted-publisher scope. TanStack's npm trusted-publisher configuration granted publish trust to any workflow run originating from the TanStack/router repository, without restricting to a specific workflow file (release.yml) or a specific ref (refs/heads/main). The attacker extracted the ACTIONS_ID_TOKEN_REQUEST_TOKEN from /proc/pid/maps and /proc/pid/mem of the Runner.Worker process during the release workflow run. Because the token's audience and subject claims matched the overly broad trusted-publisher policy, npm accepted it as a valid publish credential. The attacker then used the Sigstore stack (Fulcio certificate authority and Rekor transparency log) with the same stolen OIDC token to generate valid SLSA v1 in-toto provenance statements for each malicious package. The Sigstore attestations correctly record the repository, workflow ref, and run ID, because those values were genuine.

The payload (router_init.js) executed under the Bun runtime to evade Node.js --experimental-permission hooks and vm module monitoring. It used generateKeyPairSync and sign to forge Sigstore-compatible in-toto provenance attestations for every package it republished in the worm propagation phase, meaning secondary victims' packages also carried what appeared to be valid SLSA provenance.

The PyPI variant operated differently: 13 lines injected into __init__.py downloaded and executed a non-obfuscated modular credential stealer (transformers.pyz) from git-tanstack[.]com, targeting Linux systems only and skipping execution on Russian-locale environments. A geofenced destructive branch carried a 1-in-6 probability of executing rm -rf / on systems with Israeli or Iranian locale settings.

TECHNICAL ATTACK CHAIN🍴1Fork PR triggers pull_request_targetbundle-size.yml runs with base-repo perms💾2Malicious pnpm store written to cacheCache key matches release workflow restore🔀3Legitimate merge triggers release.ymlWorkflow restores poisoned cache on main🧠4OIDC token extracted from process memoryRunner.Worker /proc/pid/mem read directly🪙5Token exchanged for npm publish credentialBroad trusted-publisher policy accepts it6SLSA BL3 provenance generated via SigstoreFulcio and Rekor sign malicious packagesEach vulnerability is necessary. None alone is sufficient to achieve publish access.

CVE and Advisories

No CVE has been assigned to the pull_request_target cache poisoning chain as of the publication date. The underlying pull_request_target misconfiguration is a documented insecure usage pattern, not a GitHub platform vulnerability. GitHub's security hardening documentation addresses it at Keeping your GitHub Actions and workflows secure: Pwn Requests.

The npm trusted-publisher OIDC scope issue is a configuration weakness, not a platform vulnerability. npm's trusted-publishing documentation describes branch and workflow scoping at docs.npmjs.com/generating-provenance-statements.

TanStack's official postmortem is available at tanstack.com/blog/npm-supply-chain-compromise-postmortem.

MITRE ATT&CK Mapping

Technique IDATT&CK nameHow it appeared
T1195.001Supply Chain Compromise: Compromise Software Dependencies and Development ToolsAttacker poisoned the GitHub Actions cache to inject malicious dependencies into TanStack's release pipeline.
T1059.007Command and Scripting Interpreter: JavaScriptObfuscated JavaScript payload (router_init.js) executed via npm lifecycle hooks under the Bun runtime.
T1552.001Unsecured Credentials: Credentials in FilesPayload harvested credentials from cloud provider configuration files, SSH key files, Kubernetes service account token files, and IaC files.
T1003OS Credential DumpingAttacker-controlled binaries read /proc/pid/mem of the Runner.Worker process to extract the OIDC token from live process memory.
T1543.001Create or Modify System Process: Launch AgentPersistence written to macOS LaunchAgent (com.user.gh-token-monitor.plist) and Linux systemd user service.
T1485Data DestructionDead-man's switch executed rm -rf on token revocation before daemon removal.
T1567.001Exfiltration Over Web Service: Exfiltration to Code RepositoryStolen credentials exfiltrated to attacker-created GitHub repositories using stolen tokens.
T1102Web Service: Dead Drop ResolverGitHub repositories with Dune-themed names used as dead drops for command-and-control communication.

Indicators of Compromise

Network Indicators

  • C2 domain: git-tanstack[.]com
  • Session network seed node: filev2.getsession[.]org
  • GitHub API polling: api.github.com/user every 60 seconds from gh-token-monitor daemon

File Indicators

  • Payload SHA-256: ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c (router_init.js / tanstack_runner.js)
  • Malicious commit: 79ac49ee in fork zblgg/configuration (formerly voicproducoes/router)
  • GitHub account: voicproducoes (ID: 269549300, created 2026-03-19)

Persistence Indicators

  • macOS: ~/Library/LaunchAgents/com.user.gh-token-monitor.plist
  • Linux: ~/.config/systemd/user/gh-token-monitor.service
  • VS Code: .vscode/tasks.json with folder-open trigger
  • Claude Code: .claude/settings.json with SessionStart hook

npm Token Indicator

  • Token description string: IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner

Detection Note

Malicious @tanstack/* versions carry valid SLSA Build Level 3 provenance attestations. Provenance verification alone will not distinguish malicious from legitimate versions. Version number comparison against the confirmed malicious version list published in TanStack's postmortem is required.

Shared Tradecraft Markers Across All TeamPCP Waves

  • Staging repository descriptions: Shai-Hulud: Here We Go Again / A Mini Shai-Hulud has Appeared
  • Commit message marker: LongLiveTheResistanceAgainstMachines
  • Dead-drop branch names drawn from Frank Herbert's Dune universe
  • Russian-locale execution guardrail present in all waves

Attribution

StepSecurity, Wiz, Snyk, and Socket attribute the Mini Shai-Hulud campaign to TeamPCP with high confidence. Wiz's attribution is anchored to a shared RSA public key observed across the Bitwarden CLI, Checkmarx KICS, and TanStack operations. Google's Threat Intelligence Group formally tracks the operators as UNC6780, with their credential stealer designated SANDCLOCK. The group publicly claimed credit for the attack. TeamPCP is also tracked under the aliases DeadCatx3, PCPcat, ShellForce, and CipherForce.

A possible connection between TeamPCP (operational actor) and Lapsus$ (extortion actor) has been noted by SANS ISC based on timing, overlapping access vectors, and shared C2 infrastructure, but is not definitively established. No nation-state link has been formally established by any government body as of mid-May 2026.


Primary Sources