TeamPCP continues to target high-privilege tooling in the open-source supply chain. The latest incident is a Shai Hulud–style worm that has now hit over 100 npm packages, propagating through compromised CI/CD pipelines.
Initial access: tanstack/router
The entry point was a malicious pull request opened against tanstack/router (PR #7378) from a fork at github.com/zblgg/configuration. The PR succeeded because the repository had GitHub Actions' pull_request_target trigger enabled, which runs workflows with write-scoped tokens against code from untrusted forks.
The clever bit: poisoning the PNPM cache
The malicious workflow itself contained no secrets and exfiltrated nothing directly. Instead, it used its run to write into the shared GitHub Actions cache under the key:
This is the same cache key that the project's legitimate release.yml workflow uses. When release.yml later ran on a trusted branch, it restored the poisoned PNPM store, executing the attacker's code inside a workflow that had id-token: write permissions.
Token theft via /proc
Once running inside release.yml, the payload located the publishing process's PID and read:
/proc/<pid>/maps/proc/<pid>/mem
to scrape the raw OIDC token out of process memory. That token is what npm trusts for trusted-publisher publishing, so it gave the attackers the ability to push malicious versions of any package the project publishes; which is how the worm has now spread to 100+ packages.
Why this matters
pull_request_target+ shared caches is a known-dangerous combination. A workflow that looks read-only can still write to the cache and poison later privileged runs.Secrets in env vars aren't the only thing to protect; process memory of any workflow with
id-token: writeis now a target.Trusted publishing does not eliminate supply-chain risk; it just moves it from long-lived tokens to short-lived ones that can still be stolen mid-flight.
Payload analysis: router_init.js
The malicious file injected into @tanstack/react-router is router_init.js — a 2.3 MB single-line Bun-compiled JS bundle. It is never listed in the package's files field in package.json, making it easy to miss in a diff. The bundle has multiple obfuscation layers, anti-analysis checks, six credential collectors, dual exfiltration channels, and a self-replication mechanism.
Obfuscation
Two layers protect the payload from static analysis.
Layer 1 — string-array rotation (obfuscator.io style). Every string literal is replaced with a call to a lookup function backed by a shuffled array. A self-executing IIFE rotates the array to the correct offset at runtime using an arithmetic checksum; if you try to extract strings without first running the rotation, every index is wrong:
Layer 2 — beautify() AES decryption. A second function decrypts base64-encoded ciphertext at runtime. Every sensitive string — URLs, environment variable names, file paths, and the embedded Python payload — is hidden behind this call:
Anti-analysis checks
The main orchestrator runPayload() calls preflightChecks() first and exits silently if either check fires.
Russia/CIS locale check (isRussianLocale()). If the system locale or any language environment variable contains "ru", the payload exits. This is a standard for teamPCP attacks:
Sandbox/scanner detection (isSecurityScannerEnv()). Checks 16+ environment variables to detect AV sandboxes and security analysis pipelines. Variable names are all AES-encrypted in the bundle:
The payload also suppresses SIGTERM and SIGINT so it cannot be killed cleanly while exfiltration is in progress:
Credential collectors
collectAllCredentials() runs three collectors in parallel (FilesystemCollector, ShellEnvCollector, GitHubActionsCollector), then five cloud-service scanners stream their results through the same dispatcher.
FilesystemCollector : uses Bun.Glob to scan OS-specific "hotspot" file paths for GitHub tokens and npm tokens. There are 80+ encrypted paths for Linux, 40+ for macOS, and 12 for Windows, covering .npmrc, .gitconfig, .env files, shell history, and browser credential stores:
ShellEnvCollector : runs a shell command via execSync and dumps the entire process.env:
GitHubActionsCollector Only activates inside GitHub Actions on Linux (GITHUB_ACTIONS === 'true' and RUNNER_OS === 'Linux'). It pipes an AES-encrypted Python script stored in the bundle into sudo python3 via stdin. The Python script reads the Actions runner worker's internal IPC to extract every masked secret:
The five cloud-service scanners run via executeStreaming() after the initial collectors complete:
AwsStsScanner: validates stolen AWS credentials viaGetCallerIdentity; retrieves account ID, ARN, and user IDAwsSecretsManagerScanner: paginates through all secrets across all 17 AWS regions (us-east-1→eu-west-3)AwsSsmScanner: enumerates AWS SSM Parameter Store entries with exponential-backoff retryVaultScanner: tries AWS IAM auth, then token file auth (~/.vault-token,/run/secrets/VAULT_TOKEN, Kubernetes service account token), then directX-Vault-Tokenheader auth againstVAULT_ADDRGitHubRepoScanner: uses stolen GitHub tokens to enumerate accessible repositories and their secrets
Exfiltration
A dispatcher fans out all collected data to two senders simultaneously.
GitHubSender : interestingly there seems to be code present that attempts to creates a new public repository under the victim's own account using a randomly generated name (two words from encrypted wordlists joined with a number, e.g. sunset-harbor-413). Stolen data is base64-encoded and committed as files, chunked at 30 MB per file so GitHub's API limits are not hit:
SessionSender : connects to the Session decentralised messenger network as a fallback C2 channel. Bootstraps by resolving Oxen snode addresses, establishes an encrypted keypair, and uploads files to filev2.getsession.org. Session is chosen because it has no central authority that can action a takedown request — the "Couldn't fetch snodes" error strings visible in the bundle are Session's own bootstrap error messages.
Self-replication
NpmWormReplicator — for each stolen npm token, fetches the list of packages the token can publish, injects the payload into each one, and publishes. Linux/macOS only; cleans up its temp directory afterwards:
GitHubWorkflowInjector — for GitHub tokens that have workflow scope (hasWorkflowScope), pushes a malicious workflow YAML into accessible repositories so the payload re-executes on every subsequent push, continuing the worm's spread independently of npm.
IoCs
Commit:
tanstack/router@79ac49eedf774dd4b0cfa308722bc463cfe5885cSource fork:
github.com/zblgg/configurationMalicious PR:
tanstack/router#7378





