The NPM Worm Is Back And It's So Much Worse (TanStack Hacked)

BBetter Stack
컴퓨터/소프트웨어경영/리더십AI/미래기술

Transcript

00:00:00Shai Hulud is back for a fourth sequel.
00:00:02This time it's targeting packages like TanStack,
00:00:04literally hours after I published this video on Next.js,
00:00:07which was just brilliant timing by me.
00:00:08This one is actually a massive NPM supply chain attack
00:00:11that impacts more than just TanStack.
00:00:13It also got packages like UiPath, Mistral,
00:00:15and 160 other packages,
00:00:17even including PyPy packages like Guardrails.ai.
00:00:20What makes this one even more fun
00:00:22is it included a Deadman switch,
00:00:24so if it detected that you rotated your stolen keys,
00:00:26it would wipe your entire PC,
00:00:28and it even had some global politics built in too.
00:00:30So let's dive in.
00:00:36For this sequel, The Worm has the same goal,
00:00:39steal credentials from developer machines and CICD runners,
00:00:42then use those credentials to reach more packages.
00:00:44For TanStack, that meant publishing 84 malicious versions
00:00:47across 42 TanStack packages in just a few minutes.
00:00:51Now, I'll walk through how they managed
00:00:52to infect TanStack in the first place,
00:00:54but first let's see what the malware itself does
00:00:56if you were to install one of these impacted packages.
00:00:58Inside the malicious packages,
00:00:59you would find a new file called routerinit.js,
00:01:02as well as an injected optional dependency,
00:01:04which leads to what looks like
00:01:05a legitimate TanStack router GitHub link,
00:01:08but it's actually an orphan commit on the attacker's fork.
00:01:10This is just the way that GitHub handles fork links,
00:01:13so the URL can actually look like
00:01:14it belongs to the original project,
00:01:16even though the commit is actually from a fork.
00:01:18In that fork, there's a lifecycle script,
00:01:20prepare, that runs bun run task runner JS,
00:01:22and has exit one at the end.
00:01:24That's just a clever way to make the optional dependency fail
00:01:27after the payload's already run,
00:01:28so the install keeps going like normal,
00:01:30and it leaves less obvious traces in your install logs.
00:01:33Also, you may have noticed that this isn't running
00:01:35that routerinit.js file that I said was injected
00:01:37into the packages at the start,
00:01:38but for now, just think of these two files
00:01:40as playing the exact same role with different names.
00:01:42The TLDR is, when you install this,
00:01:44it's going to run this script.
00:01:46The first thing that script tries to do
00:01:47is decouple itself from the obvious install flow,
00:01:50so it will check whether it's already running
00:01:51in the background, and if it's not,
00:01:53it forks a detached copy of itself
00:01:54and exits the parent script cleanly.
00:01:57This way, your npm install logs
00:01:58don't show any of the script's output
00:02:00because the malware's now detached itself
00:02:01from that process and is running in the background.
00:02:04After this, it does something really clever.
00:02:06It writes copies of itself
00:02:07into your clawed code hooks directory,
00:02:08then configures your clawed settings
00:02:10to run this hook anytime you use clawed code
00:02:12in that project.
00:02:13This way, it can survive past the original install
00:02:16and keep re-triggering every time
00:02:17you open clawed code in that project.
00:02:20It actually does the same with VS Code's task runner,
00:02:22duplicating itself in there,
00:02:23so if you use VS Code's workspace auto-run features,
00:02:26you have the exact same problem.
00:02:28It even sets up an OS-level service
00:02:29called GitHub Token Monitor,
00:02:31but we'll come back to that one
00:02:32because that one is absolutely diabolical.
00:02:34It's also pretty diabolical
00:02:35that you haven't subscribed yet.
00:02:37The next thing the payload does
00:02:38is it gets to work stealing your credentials,
00:02:40and it tries everything.
00:02:41In GitHub Actions, it looks for credentials
00:02:43and secrets in the runner environment.
00:02:45More specifically, scraping the GitHub Actions
00:02:47runner worker process memory
00:02:48for your workflow secrets
00:02:50that include mask secrets,
00:02:52and it even inserts a fake code QL-looking GitHub workflow
00:02:55that serializes your repo secrets
00:02:57and exfiltrates them later.
00:02:58It also looks for AWS secrets,
00:03:00first going after your environment variables
00:03:02and local config files,
00:03:03but then it also goes after AWS metadata services
00:03:06like IMDS v2 and ECS task metadata.
00:03:09For Kubernetes, it steals service account tokens
00:03:11and certificates which allows it in-cluster API access
00:03:14to whatever role-based access control privileges
00:03:17that pod's service account had,
00:03:19which in badly configured clusters
00:03:21can be extremely broad,
00:03:22sometimes effectively admin.
00:03:24And to make that even worse,
00:03:25it also goes after HashiCorp Vault,
00:03:27gathering all your vault-related environment variables
00:03:29and tokens,
00:03:30then uses whatever Kubernetes access it has
00:03:32to retrieve all of your vault-managed secrets.
00:03:34And all of that's just what it does
00:03:35to your CI deployments.
00:03:37If it's on your workstation,
00:03:38it goes after all your SSH keys,
00:03:39your NPM credentials,
00:03:41your Git credentials,
00:03:42shell history,
00:03:43cloud provider credentials,
00:03:44crypto keys,
00:03:45signal,
00:03:45Slack,
00:03:45and Discord files.
00:03:46And on top of all of that,
00:03:47it extracts your clawed code session history.
00:03:49So if you've ever given clawed credentials
00:03:51or let it read files containing credentials,
00:03:53it has access to those too.
00:03:55So yeah, as I said,
00:03:56they were after absolutely everything
00:03:57that they could get their hands on,
00:03:58and then they would exfiltrate this data
00:04:00via the session messenger network.
00:04:02And as a backup,
00:04:02they also dead-dropped this stolen data
00:04:04into GitHub repos.
00:04:05And in theme with all of their attacks,
00:04:07these branches are named after Dune references.
00:04:09So it's got your credentials.
00:04:11It can't get any worse, right?
00:04:12Well, yes.
00:04:13Yes, it can.
00:04:14On top of all of that,
00:04:15if you remember that service
00:04:16that I said it sets up on your machine,
00:04:18well, that one monitors your GitHub tokens
00:04:19and it keeps re-exfiltrating them.
00:04:21But also every minute,
00:04:22it checks if the token is still valid.
00:04:24And if it's not,
00:04:25it runs RMRF on your user directory,
00:04:27wiping everything.
00:04:28It also tries to create an NPM token
00:04:30with your credentials,
00:04:31with the description,
00:04:32if you revoke this token,
00:04:33we will wipe the computer of the owner,
00:04:35implying that it does the same thing
00:04:36for NPM tokens too.
00:04:38So if you revoke these tokens
00:04:39before isolating your machine
00:04:40and removing that background process,
00:04:42the payload can self-destruct your PC,
00:04:44which is just absolutely diabolical.
00:04:46And as a side note here,
00:04:47the Python variant of this attack
00:04:48does roughly the same thing,
00:04:49but it also includes a check
00:04:51to see if your machine's language is Russian.
00:04:53If it is,
00:04:53it just stops.
00:04:54And if your machine appears to be
00:04:55from Israel or Iran,
00:04:56it generates a random number
00:04:58between 1 and 6.
00:04:59And if that number is 2,
00:05:00it runs a destructive wipe command
00:05:01and tries to full-volume play
00:05:03an MP3.
00:05:04Sadly,
00:05:05I couldn't find out
00:05:05what that MP3 is.
00:05:07Anyways,
00:05:07now it's done all that,
00:05:08the worst is still yet to come
00:05:09because that was only stage 1.
00:05:11Stage 2 is self-propagation
00:05:13and that's the most dangerous part
00:05:15of this attack.
00:05:16First,
00:05:16it will look on your machine
00:05:17for any valid NPM tokens
00:05:19where it can publish
00:05:19without two-factor authentication.
00:05:21And if it finds one,
00:05:22it will scan all of the packages
00:05:24that that account has access to,
00:05:26then use those credentials
00:05:26to add itself to those packages
00:05:28and publish new infected versions.
00:05:30Now that's obviously pretty bad,
00:05:32but you also probably shouldn't
00:05:33have published tokens
00:05:33sitting around
00:05:34that can bypass
00:05:35two-factor authentication.
00:05:36So the much scarier version of this
00:05:38is what happens
00:05:39when it runs inside your CI-CD.
00:05:41Because in CI,
00:05:42the attacker doesn't need
00:05:43a long-lived NPM token
00:05:44because good setups
00:05:45often rely on OIDC,
00:05:47which is meant to be safer.
00:05:48Essentially,
00:05:49instead of storing
00:05:50an NPM token as a secret,
00:05:51GitHub Actions proves to NPM,
00:05:53hey,
00:05:53I'm this repo
00:05:54running this workflow
00:05:55on this branch,
00:05:56and NPM then gives it
00:05:57a short-lived published token.
00:05:59The problem with this though
00:06:00is if the script gets access
00:06:01to a trusted GitHub Actions environment,
00:06:03it can stand in the same place
00:06:04as a legitimate publisher.
00:06:06So the malware can use
00:06:07the OIDC-related environment
00:06:08that GitHub exposes to the job
00:06:10to request an OIDC JWT token
00:06:12from GitHub's token endpoint,
00:06:14then it exchanges that JWT token
00:06:16with NPM
00:06:17for a short-lived published token
00:06:18through the NPM-trusted
00:06:19publishing system,
00:06:20and now it can publish
00:06:22without ever stealing
00:06:22a permanent NPM token
00:06:24and look completely legitimate.
00:06:26In this case,
00:06:26the malware bundles a copy
00:06:27of that router init.js file
00:06:29into the package's table,
00:06:30then adds the malicious
00:06:31optional dependency,
00:06:32then publishes it all
00:06:33as the latest tag
00:06:34for that package,
00:06:35so when someone
00:06:35or some CICD pipeline
00:06:37installs those packages,
00:06:38the loop starts over again,
00:06:40spreading as far
00:06:40as it possibly can.
00:06:42So that's all
00:06:42pretty insane, right?
00:06:43But now let's focus
00:06:44on patient zero,
00:06:46TanStack.
00:06:46How did they get infected
00:06:47in the first place?
00:06:48Well,
00:06:49according to their own post-mortem,
00:06:50the attacker abused
00:06:51that GitHub Actions pipeline.
00:06:53They started the day
00:06:53before the malicious packages
00:06:54were actually published,
00:06:56where they created a fork
00:06:57of TanStack router,
00:06:58but they actually renamed
00:06:59this to configuration
00:06:59to try and make it harder
00:07:01to find if you were searching
00:07:02through the obvious fork names.
00:07:04Then they added
00:07:04a malicious commit
00:07:05to this fork,
00:07:06which they fake-authored
00:07:07as Claude,
00:07:07and it had a commit message
00:07:08that was prefixed
00:07:09by skip CI,
00:07:10so it wouldn't immediately
00:07:11run CI on a push event.
00:07:13The next day,
00:07:13they then opened a PR
00:07:14against TanStack router
00:07:15called Work in Progress
00:07:16Simplify History Build.
00:07:18And this is where
00:07:18the actual attack happens.
00:07:20The TLDR is that
00:07:21TanStack had a bundle-sized
00:07:22GitHub Actions workflow
00:07:23that used pull request target,
00:07:25and that's notable
00:07:26because pull request target
00:07:27actually runs
00:07:28in the base repo
00:07:29security context,
00:07:30not the fork.
00:07:31That means that it has access
00:07:32to the base repo's cache scope
00:07:33and its GitHub token.
00:07:35So this workflow
00:07:35checked out the PR,
00:07:36installed its dependencies,
00:07:38and ran a benchmark build.
00:07:39The trouble is, though,
00:07:40that fork contained
00:07:40malicious code.
00:07:41In this case,
00:07:42it was a V setup script
00:07:43that poisoned the
00:07:44PMPM package store
00:07:45under the exact cache key
00:07:47that the release action
00:07:48would later use.
00:07:49They actually pre-computed
00:07:50this from the public
00:07:51PMPM lock file
00:07:52using the exact same formula
00:07:54that the workflow also uses.
00:07:56Once that poisoned cache
00:07:57was saved,
00:07:57they actually reset
00:07:58that branch back
00:07:59to match the current
00:07:59main branch,
00:08:00so the visible PR
00:08:01looked like a zero file
00:08:02no-op,
00:08:03and then they closed that PR
00:08:04and deleted
00:08:05the malicious branch.
00:08:06So from the outside,
00:08:07it looks like
00:08:07absolutely nothing
00:08:08has happened,
00:08:09but they've poisoned
00:08:10that GitHub action's cache.
00:08:11This means that
00:08:12eight hours later,
00:08:13when a normal maintainer
00:08:14merged an unrelated PR
00:08:15into main,
00:08:16it triggered Tanstack's
00:08:17release workflow,
00:08:18which restored the
00:08:19PMPM poisoned cache,
00:08:20and now attacker control code
00:08:22was running inside
00:08:23that release action.
00:08:24It then used the same logic
00:08:25with OIDC
00:08:26to get an NPM publishing token,
00:08:28and managed to publish
00:08:2984 versions of itself
00:08:30across 42 Tanstack packages,
00:08:32and it didn't even need
00:08:33to get to the
00:08:34published package step
00:08:35of the action.
00:08:36Funnily enough,
00:08:36the action actually failed
00:08:37because some tests failed,
00:08:39so it never reached that step,
00:08:40but the malicious code ran
00:08:41and published all of them
00:08:43regardless.
00:08:43So the attacker managed
00:08:44to chain three trust boundaries.
00:08:46First,
00:08:47the fork PR code
00:08:47got to poison
00:08:48the base repos cache,
00:08:49then that base repos cache
00:08:51got restored inside
00:08:52the real release workflow,
00:08:53then the real release workflow
00:08:54has OIDC permissions,
00:08:56which turn into
00:08:57NPM publish access,
00:08:58so they can publish
00:08:59what looks to be
00:08:59completely legitimate packages.
00:09:01And that's what I think
00:09:02is getting really scary
00:09:03about the supply chain attacks.
00:09:05They're moving away
00:09:05from stealing
00:09:06one maintainer's token
00:09:07to abusing
00:09:08the entire CI-CD system itself,
00:09:10and that means
00:09:11that all of our trust signals
00:09:12are starting to work
00:09:13for the attacker.
00:09:14This was a signed package
00:09:15with valid provenance
00:09:16published by a real workflow.
00:09:18So there we go,
00:09:19that is ShaiHalud4,
00:09:20and if you want to check
00:09:21if you've been compromised
00:09:21by any of these packages,
00:09:23I'll leave links
00:09:23to blog posts down below,
00:09:25which will cover
00:09:25how you can find out
00:09:26and what you can do
00:09:27if you have installed
00:09:28one of these.
00:09:29Let me know in the comments
00:09:30what you think
00:09:30about all of this
00:09:31and the NPM ecosystem,
00:09:33while you're down there,
00:09:33subscribe,
00:09:34and as always,
00:09:34see you in the next one.

Key Takeaway

Modern supply chain attacks now target CI/CD infrastructure directly, chaining trust boundary exploits like cache poisoning and OIDC token abuse to publish malicious packages that appear to have valid provenance.

Highlights

  • The supply chain attack compromised over 160 packages, including TanStack, UiPath, and Guardrails.ai.

  • Attackers published 84 malicious versions across 42 TanStack packages within minutes.

  • The malware uses a 'deadman switch' that wipes the user's home directory if it detects stolen key rotation.

  • The payload persists on workstations by injecting itself into Claude Code and VS Code task runner hooks.

  • Attackers poisoned the GitHub Actions cache to execute malicious code during legitimate release workflows.

  • The exploit bypasses traditional security by using OIDC to acquire short-lived, legitimate publishing tokens from npm.

Timeline

Scope and Impact of the Attack

  • The attack targeted 160+ NPM and PyPy packages, including major libraries like TanStack and UiPath.
  • A built-in deadman switch triggers a complete wipe of the user's PC upon detection of stolen key rotation.
  • The primary objective is the theft of credentials from developer workstations and CI/CD runners.

This supply chain attack represents a significant escalation in complexity, extending beyond single package compromises to affect a broad ecosystem. The inclusion of a deadman switch and geopolitical conditional logic adds destructive capabilities to the standard credential theft model. Attackers successfully leveraged these methods to gain deep access to CI/CD environments.

Malware Persistence and Credential Theft

  • Persistence is achieved through hooks in Claude Code and VS Code task runners, ensuring re-execution.
  • The malware scrapes GitHub Actions runner memory, environment variables, Kubernetes service accounts, and HashiCorp Vault secrets.
  • Exfiltration occurs via the Session messenger network and hidden branches in GitHub repositories.

Upon installation, the payload detaches itself from the npm process to avoid detection in install logs. It proactively searches for sensitive data across a wide range of platforms including cloud metadata services, SSH keys, and local application files like Slack and Discord. The GitHub Token Monitor service continuously validates stolen tokens and executes destructive commands if they are revoked.

Self-Propagation via CI/CD Trust Exploitation

  • The malware attempts to publish new infected versions using stolen npm tokens or OIDC-derived credentials.
  • OIDC integration allows the malware to request short-lived publishing tokens from npm as if it were a legitimate CI process.
  • This automated propagation cycle allows the malware to spread rapidly across dependent packages and pipelines.

The attack moves from simple workstation theft to systemic propagation by abusing CI/CD trust. By impersonating legitimate publishing workflows, the malware manages to distribute infected code while maintaining the appearance of authorized, signed provenance.

Patient Zero: TanStack Pipeline Poisoning

  • Attackers poisoned the base repository's cache using a malicious PR that leveraged the 'pull_request_target' security context.
  • The cache was pre-computed to match the workflow's specific formula, ensuring it would be restored during subsequent builds.
  • Malicious code executed within the legitimate release action because it relied on the compromised cache.

The initial infection of TanStack occurred when attackers created a malicious branch to poison the GitHub Actions cache. This cache was later restored during a routine merge by a maintainer, triggering the execution of malicious code within the release workflow. The attack successfully chained three trust boundaries: PR fork code, cache restoration, and OIDC permissions, to achieve unauthorized package publication.

Community Posts

No posts yet. Be the first to write about this video!

Write about this video