Jeśli instalowałeś pakiety @tanstack/router, @tanstack/start lub @tanstack/history 11 maja po 19:20 UTC — rotuj tokeny GitHub, klucze AWS, GCP, Kubernetes, Vault i SSH. Czyste: @tanstack/query, @tanstack/table, @tanstack/form. Adresy do blokowania: filev2.getsession.org, seed{1,2,3}.getsession.org.
Tanner Linsley opublikował postmortem który jest rzadkością w branży — nie dlatego że jest szczegółowy, ale dlatego że jest szczery w miejscu gdzie większość postmortemów jest tylko precyzyjna. Napisał wprost, co go zaskoczyło najbardziej.
Złośliwe pakiety miały ważną proweniencję SLSA Level 2.
SLSA to framework który przez kilka lat był budowany jako odpowiedź na ataki przez łańcuch dostaw. Główna obietnica: kryptograficznie weryfikowalna ścieżka od kodu do pakietu. Dowód że pakiet pochodzi z właściwego repozytorium, właściwego przepływu pracy, właściwego commita.
11 maja proweniencja była prawdziwa. Pakiet rzeczywiście pochodził z TanStack/router, z release.yml, z tokenu OIDC wydanego dla właściwego przepływu na właściwej gałęzi. Wszystko się zgadzało. Framework który miał certyfikować bezpieczeństwo łańcucha dostaw — certyfikował atak.
To nie był błąd w SLSA. To coś trudniejszego.
Osiem godzin oczekiwania
10 maja wieczorem atakujący zakłada fork TanStack pod nazwą celowo niepasującą do wzorców wyszukiwania. Tworzy złośliwy commit jako claude <claude@users.noreply.github.com> — sfabrykowany adres naśladujący automatyczne commity Claude Code. W środowiskach które używają tego narzędzia commit taki jest nieodróżnialny od normalnej aktywności.
11 maja otwiera PR. Automatycznie uruchamia się bundle-size.yml. Godzinę i czterdzieści minut później zatruty cache czeka pod kluczem identycznym z tym którego release.yml będzie szukał przy następnym wydaniu. Atakujący cofa PR do HEAD — widoczna różnica to zero plików.
I nic. Przez prawie osiem godzin.
O 19:15 UTC maintainer robi zwykły merge nierelatowanego PR. release.yml uruchamia się, przywraca cache, trafia na zatrutą wersję. O 19:20:39 UTC pierwsze złośliwe pakiety są na npm z ważną proweniencją SLSA.
Dlaczego OIDC nie zatrzymał ataku
OIDC trusted publisher zastąpił statyczne tokeny npm — tokeny które można ukraść i używać z dowolnego miejsca. Zamiast trwałego klucza: krótkotrwały token, specyficzny dla konkretnego przepływu, wydawany tylko w momencie potrzeby.
Linsley opisuje problem jednym zdaniem: "Once configured, any code path in the workflow can mint a publish-capable token."
Token jest wydawany gdy runner o niego prosi. Każdy kod działający na runnerze z uprawnieniem id-token: write może ten token wydobyć. Cache poisoning wstawił złośliwy kod dokładnie tam — na chwilę przed tym momentem.
Przenieśliśmy powierzchnię ataku z "token w sekretach" na "kod w środowisku z id-token: write." Adres się zmienił. Rozmiar nie.
Skrypt z komentarzem atrybucji
Jest jeden szczegół w postmortemie który jest jednocześnie techniczny i niemal absurdalny.
Skrypt Python do wydobywania tokenu OIDC z pamięci runnera przez /proc/<pid>/mem był dosłownie tym samym skryptem który pojawił się przy kompromitacji tj-actions/changed-files z marca 2025. Z zachowanym komentarzem atrybucji.
Atakujący skopiował opublikowane badanie razem z adnotacją skąd pochodzi. Technika działa — i jest dostępna dla każdego kto przeczyta właściwe artykuły z zeszłego roku.
Trzy szczęścia
Postmortem ma sekcję której rzadko się widuje: co zadziałało na korzyść ofiary.
Ładunek łamał testy. Dlatego publikacja była głośna i wykryta w 20 minut. Skrypt miał komentarz atrybucji, co przyspieszyło identyfikację IOC. Ładunek w pakietach UiPath i Mistral nie działał przez błąd w implementacji — efektywna kompromitacja ograniczyła się do TanStack.
Trzy szczęścia w jednym incydencie. Następna iteracja może nie mieć żadnego.
Luka między tym czego pilnują a tym co trzeba chronić
SLSA i OIDC trusted publisher są lepsze od tego co zastąpiły. To nie jest kwestia sporna.
Ale oba zostały zaprojektowane przy założeniu że środowisko wykonania jest godne zaufania. Że runner który prosi o token OIDC robi to w imieniu kodu który powinien ten token dostać. Cache poisoning wszedł przed tym momentem. Proweniencja SLSA zarejestrowała to co wydarzyło się po nim.
Atak na TanStack nie złamał ani jednego mechanizmu. Znalazł szczelinę między tym czego pilnują a tym co faktycznie wymagało ochrony.
Konkretne poprawki są w postmortemie — izolacja pull_request_target, ręczne zarządzanie kluczami cache, izolacja kroku z id-token: write. Warto je wdrożyć. Ważniejsze pytanie brzmi przy jakich założeniach zostały zaprojektowane frameworki bezpieczeństwa łańcucha dostaw których używamy dziś — i gdzie te założenia przestają obowiązywać.
TanStack pokazał jedno takie miejsce. Nie będzie ostatnie.
Źródła
TanStack Blog — pełny postmortem: https://tanstack.com/blog/npm-supply-chain-compromise-postmortem
Snyk — analiza OIDC token extraction: https://snyk.io/blog/tanstack-npm-packages-compromised/
Wiz — aktualizacja o błędzie ładunku UiPath i Mistral: https://www.wiz.io/blog/mini-shai-hulud-strikes-again-tanstack-more-npm-packages-compromised
StepSecurity — pierwotne wykrycie: https://www.stepsecurity.io/blog/mini-shai-hulud-is-back-a-self-spreading-supply-chain-attack-hits-the-npm-ecosystem















































































































Nie nowy atak, tylko naprawiony błąd. Co łatka Gemini CLI mówi o tym, że tryb –yolo w potoku CI/CD to nie jest dobry pomysł