TanStack Compromised: Inside the GitHub Actions Cache Poisoning That Hit the npm Ecosystem

RONI CARTA | LUPIN

47+

packages hit

25

days early

On May 11, 2026, a sophisticated supply chain worm tore through the npm ecosystem, compromising over 47 packages spanning TanStack, UiPath, and several other organizations. The attack wasn’t loud. It was surgical. And it had been fully mapped by Depi weeks before it was ever executed.

All Depi clients were automatically alerted at the moment the vulnerability was detected. This is what the full chain looked like.

The Entry Point: PR #7378

The attack began with a seemingly ordinary pull request, PR #7378, submitted to the TanStack/router repository. The attacker’s goal was not to merge code into the main branch directly. Instead, they were exploiting a structural flaw in the repository’s GitHub Actions configuration: the combination of a pull_request_target workflow and an unsafe checkout of fork code.

bundle-size.yml, a workflow responsible for benchmarking bundle sizes on pull requests, was triggered by the pull_request_target event. This is one of the most dangerous events in GitHub Actions. Unlike pull_request, which runs in a sandboxed environment with restricted permissions, pull_request_target runs in the context of the base branch and has full access to repository secrets and GitHub Actions cache credentials.

The benchmark-pr job checked out the PR’s merge commit directly:

- uses: actions/checkout@v6.0.2
  with:
    ref: refs/pull/${{ github.event.pull_request.number }}/merge

This handed the attacker’s code execution rights in a fully privileged environment: no reviewer approval required, no interaction from maintainers needed. Attack complexity: zero.

The Pivot: Post-Checkout Script Clobbering

Once code execution was achieved, the attacker pivoted using a technique similar to the previously documented Cacheract by Adnan Khan: clobbering the post-checkout hook of actions/checkout to detonate the payload after the main job steps complete.

The fork code, loaded during the build via packages/history/vite_setup.mjs, overwrote the post script of actions/checkout@v6.0.2. The post step runs at the end of the job, after all workflow steps have completed, and by that point the runtime had already injected the following environment variables into the context:

  • ACTIONS_CACHE_URL: the endpoint to read and write from the GitHub Actions cache
  • ACTIONS_RUNTIME_TOKEN: the bearer token authorizing those cache operations

The clobbered post-checkout script used these credentials to overwrite the shared pnpm store cache under the default branch scope, using the exact same cache key format used by every other workflow in the repository:

${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}

Because pull_request_target runs in the base branch context, the poisoned cache entry was written to the main branch scope, not just for this PR, but for every subsequent workflow run in the repository.

The Full Attack Chain

Depi’s graph engine traced the exploit end-to-end across two workflows.

The poisoner: bundle-size.yml, triggered by pull_request_target:

WorkflowNode('TanStack/router:main:.github/workflows/bundle-size.yml')
  └── JobNode('benchmark-pr' #L28)  [trigger: pull_request_target]
        └── StepNode('Checkout_0' #L35)
              ref: refs/pull/${{ github.event.pull_request.number }}/merge  ← fork code
        └── StepNode('Setup Tools' #L42)
              └── ActionNode('TanStack/config/.github/setup@main')
                    └── cache credentials exposed to fork code
        └── StepNode('Measure Bundle Size' #L45)
              └── SINK: pnpm nx run @benchmarks/bundle-size:build  ← fork code executes

The victim: release.yml, triggered on every push to main:

WorkflowNode('TanStack/router:main:.github/workflows/release.yml')
  └── JobNode('release' #L21)  [trigger: push]
        └── StepNode('Checkout_0' #L26)
              └── ActionNode('actions/checkout@v6.0.2')
        └── StepNode('Check for changesets' #L30)
        └── StepNode('Start Nx Agents' #L39)
        └── StepNode('Setup Tools' #L42)
              └── ActionNode('TanStack/config/.github/setup@main')
                    └── StepNode('Setup pnpm' #L6)
                    └── StepNode('Setup Node' #L8)
                    └── StepNode('Get pnpm store directory' #L13)
                    └── StepNode('Setup pnpm cache' #L17)  ← restores poisoned cache
                    └── StepNode('Install dependencies' #L24)
                          └── SINK: pnpm install --frozen-lockfile  ← poisoned

release.yml carries contents: write and id-token: write permissions and runs pnpm run changeset:publish, the npm publish step. A poisoned pnpm store restored here means backdoored packages ship directly to the registry on the next merge to main.

The permissions escalation path: contents: read → write, pull-requests: none → write, id-token: none → write.

TanStack Analysis

The Worm: Self-Spreading Through the Ecosystem

Once the first batch of malicious packages was published, the attack became self-propagating.

Each compromised package shipped a 2.3 MB obfuscated binary credential stealer. On installation in any CI/CD environment, it harvested:

  • GitHub tokens
  • npm authentication credentials
  • Any secret injected into the pipeline environment

Those stolen tokens were immediately used to publish new malicious versions of every package the victim token had write access to. Ten malicious @tanstack versions were published within a six-minute window beginning at approximately 19:20 UTC on May 11.

The worm then jumped to adjacent organizations whose pipelines had installed TanStack packages.

Blast Radius: 47+ Packages Across the Ecosystem

The following packages received malicious versions. All were published in pairs (e.g., 1.169.5 / 1.169.8, 1.166.12 / 1.166.15) to appear as routine patch releases:

@tanstack (35 packages)

@tanstack/router-core, @tanstack/react-router, @tanstack/vue-router, @tanstack/solid-router, @tanstack/router-devtools, @tanstack/history, and 29 additional packages across the TanStack Router and adjacent tooling ecosystem.

@uipath (7 packages)

@uipath/docsai-tool, @uipath/packager-tool-apiworkflow, @uipath/agent.sdk, and four additional packages.

Additional targets

@draftauth/client, @draftlab/auth, @taskflow-corp/cli, @tolka/cli, and others.

A critical detail: the malicious packages carried valid SLSA provenance attestations. Standard supply-chain integrity checks would not have flagged them.

Depi Detected the Full Chain on April 16

At Thu, 16 Apr 2026 21:34:36 CEST, Depi autonomously traced and flagged the complete exploit chain described above, 25 days before the attacker pulled the trigger.

The vulnerability was not a known CVE. There was no public disclosure. No one had reported anything. Depi identified it by analyzing the structural relationship between bundle-size.yml and the release workflows: a pull_request_target workflow that executed fork code in a privileged context, sharing a cache namespace with a release-capable workflow. The combination was unambiguous.

All Depi clients using any @tanstack package referencing the vulnerable repository as the provenance were alerted immediately, with full exploit chain details, blast radius mapping, and pinning recommendations, before a single malicious byte hit npm.

TanStack Attack Path

Timeline

  • Thu, Apr 16, 2026 at 21:34:36 CEST: Depi detects and maps the full exploit chain in TanStack/router
  • Apr 16-24, 2026: All Depi clients alerted; pinning strategies issued
  • May 11, 2026 at ~19:14 UTC: Attacker submits PR #7378; bundle-size.yml executes fork code
  • May 11, 2026 at ~19:20 UTC: Poisoned cache written to main branch scope
  • May 11, 2026 at ~19:20-19:26 UTC: 10 malicious @tanstack versions published in 6 minutes
  • May 11, 2026: Worm propagates to UiPath and additional targets; 47+ packages total compromised

What This Means

This attack is notable for three reasons.

First, it required zero interaction from maintainers. No social engineering, no account takeover, no phishing. A single pull request was sufficient.

Second, the self-spreading mechanism turned a single repository compromise into an ecosystem-wide worm within minutes. Any organization whose CI/CD pipeline installed a TanStack package during that window was a potential next target.

Third, the presence of valid SLSA attestations on the malicious packages is a signal that provenance alone is not a sufficient defense. Attestations verify that a package was built by a specific pipeline, but if that pipeline is compromised, attestations become a trust amplifier, not a safeguard.

The only reliable detection is upstream behavioral analysis: modeling what a repository’s workflows are capable of before an attacker acts on that capability.

That is what Depi is built for.

More Research

  • Threat Intelligence
    npm

    node-ipc Compromised: A Dormant Maintainer, an Expired Domain, and What We Think Happened

    node-ipc 12.0.1 shipped a DNS-exfiltrating infostealer, published by a maintainer dormant 1,476 days whose contact domain had lapsed and been re-registered.

    RONI CARTA | LUPIN
    Read the research
  • Security Research
    GitHub Actions

    Find Comment, Get Shell: Command Injection in dbt’s GitHub Actions

    An attacker-posted issue comment raced dbt automation and landed in a Bash conditional, leaking FISHTOWN_BOT_PAT. Exploit chain built by an AI agent.

    V.M.
    Read the research
  • Security Research
    npm

    npx Used Confusion and It’s Super Effective

    npx installs an unclaimed package when a bin name does not resolve locally. Claiming those names on the public registry earned five-figure RCE bounties.

    V.M.
    Read the research

The next incident is already in your upstream chain.

30 minutes with our team. Bring your most critical repo and we'll show you what's hiding in its upstream security, live, on the call, before you sign anything.