W lipcu opisywaliśmy robaka npm, który instalował podstawiony serwer MCP w konfiguracjach asystentów kodujących, i cytowaliśmy nazwę, jaką nadał tej klasie CrowdStrike: living off the AI toolchain. Kampania miała wtedy dziewiętnaście pakietów i badacze traktowali ją raczej jako dowód koncepcji nowej klasy niż jako realne zagrożenie.
Dziś o 09:35 czasu uniwersalnego opublikowano złośliwe wydanie pakietu keyv. W ciągu następnego pół godziny ta sama klasa ataku rozeszła się na ponad czterysta pakietów w dziewięciu niepowiązanych organizacjach.
A najciekawsze w tej historii jest to, że po drodze nie złamano ani jednego zabezpieczenia. Wszystkie zadziałały dokładnie tak, jak zaprojektowano.
Trzydzieści minut
keyv to biblioteka do przechowywania par klucz–wartość, pobierana około stu dwudziestu siedmiu milionów razy tygodniowo. Jej opiekun utrzymuje też kilka innych narzędzi z tej samej rodziny: flat-cache z pięciuset sześćdziesięcioma pięcioma milionami pobrań miesięcznie, file-entry-cache z pięciuset pięćdziesięcioma siedmioma, cacheable z dwudziestoma dziewięcioma. Atakujący przejęli jego konto GitHub, wypchnęli złośliwe pliki wprost do gałęzi głównej i natychmiast wycięli nowe wydanie.
Od tego momentu rozprzestrzenianie było automatyczne. Robak kradł poświadczenia publikacyjne każdej ofiary i republikował jej cały namespace w tempie mniej więcej jednego pakietu na sekundę, przeskakując do kolejnej organizacji co dwie do siedmiu minut. W pół godziny dotarł do dziewięciu niezależnych podmiotów, wśród nich Deliveroo, Qlik i ServiceTitan. Wśród zatrutych pakietów znalazły się biblioteki uwierzytelniające.
Liczby wciąż się rozjeżdżają, co jest normalne przy telemetrii zbieranej w trakcie trwania kampanii, a nie sprzecznością między zespołami. Wiz mówi o ponad czterystu odrębnych pakietach. SafeDep, licząc bezpośrednio wobec rejestru, doszedł do tysiąca sześciuset osiemdziesięciu czterech zatrutych wersji w czterystu dwudziestu nazwach. Socket podaje czterysta czterdzieści dwie wersje w trzystu pięćdziesięciu trzech nazwach, Aikido co najmniej osiemset sześćdziesiąt osiem pakietów w tysiącu trzystu osiemdziesięciu jeden wersjach. Żadnej z szerszych sum nie dało się w chwili raportowania niezależnie odtworzyć z pełnej publicznej listy — i to zastrzeżenie warto zachować, bo skala jest tu przedmiotem szacunku, nie pomiaru.
Mechanizm wejścia jest przy tym rozbrajająco prosty. Każde zatrute wydanie dodaje jedną linijkę do manifestu pakietu: "preinstall": "node setup.mjs". Ładunek o rozmiarze siedmiuset dwudziestu siedmiu kilobajtów uruchamia się przy zwykłym npm install, zanim wykona się jakikolwiek własny kod projektu. Zbiera tokeny GitHub, poświadczenia chmurowe, klucze prywatne i parametry połączeń do baz danych, a potem używa zdobytych uprawnień publikacyjnych, żeby zatruć kolejne pakiety.
Wszystko było podpisane
Teraz sedno, bo to jest miejsce, w którym ta historia przestaje być kolejną kampanią w łańcuchu dostaw.
Przez ostatnie lata branża zbudowała wokół rejestrów pakietów całą warstwę mechanizmów zaufania. Atestacje npm. Trusted publisher w GitHub Actions. Provenance, czyli poświadczenie, że artefakt powstał w określonym procesie budowania, z określonego repozytorium. Podpisy kryptograficzne. Sens tego wszystkiego był jeden: żeby dało się odróżnić pakiet, który przyszedł skądś wiarygodnego, od pakietu podrzuconego przez kogoś obcego.
Złośliwe wydanie keyv miało to wszystko. Ważną atestację npm. Poprawne poświadczenia trusted publishera. Provenance podpisane przez GitHub Actions. Przeszło standardowe kontrole podpisu, bo przeszło je uczciwie — artefakt naprawdę powstał w tym repozytorium, naprawdę przeszedł przez ten proces budowania, naprawdę został wydany przez uprawnione konto. Wszystko się zgadzało.
Ten sam wzorzec powtórzył się przy hookach dla środowisk programistycznych: zielona plakietka weryfikacji przypisywała je botowi, bo GitHub podpisuje to, co każe mu podpisać jego własne API. Recenzent sprawdzający którykolwiek z tych sygnałów nie znalazłby w tym incydencie niczego niepokojącego.
Powód jest strukturalny i wart nazwania wprost, bo dotyczy każdego mechanizmu tego typu, nie tylko npm. Podpis potwierdza pochodzenie, nie intencję. Mówi, skąd coś przyszło i przez jaki proces przeszło — nie mówi, czy jest bezpieczne. Dopóki zakładamy, że konto opiekuna jest w rękach opiekuna, te dwie rzeczy z grubsza się pokrywają. W chwili, gdy ktoś przejmie to konto i wypchnie zmianę do gałęzi głównej, cała maszyneria zaufania działa dalej bez zmrużenia oka — i podpisuje malware z taką samą pewnością, z jaką podpisała poprzednie wydanie.
To jest ten sam kształt problemu, który opisywaliśmy przy fałszywym skillu, który przeszedł skanery Cisco i NVIDII. Tam skan był migawką, a treść zmieniła się po jego wykonaniu. Tutaj podpis jest poświadczeniem procesu, a proces został przejęty. W obu przypadkach kontrola zadziałała poprawnie i w obu okazała się mierzyć coś innego, niż potrzebowaliśmy.
Persystencja przeniosła się do narzędzi AI
Drugi filar tej kampanii jest tym, przez który pisaliśmy o niej już w lipcu — tyle że wtedy w skali dowodu koncepcji.
Poza ładunkiem uruchamianym przy instalacji robak zostawia w repozytorium osobne hooki dla Claude Code i Visual Studio Code. Są to pliki umieszczone w katalogach .claude/ i .vscode/, między innymi setup.mjs oraz wpisy w tasks.json, które uruchamiają ładunek w momencie, gdy deweloper zaufa przestrzeni roboczej albo przyzna odpowiednie uprawnienia.
Trzeba to postawić precyzyjnie, żeby nie wyszło z tego oskarżenie pod niewłaściwym adresem: to nie jest luka w Claude Code ani w VS Code. To jest nadużycie mechanizmu zaufania przestrzeni roboczej, który oba narzędzia mają z założenia i który istnieje po to, żeby projekt mógł skonfigurować własne zadania i automatyzacje. Deweloper otwiera katalog, potwierdza, że ufa jego zawartości — i od tej chwili zdefiniowane w nim zadania mogą się wykonać. Tak to działa i tak ma działać.
Konsekwencja praktyczna jest jednak niewygodna. Usunięcie zatrutej zależności i przywrócenie czystej wersji pakietu nie kończy sprawy, bo ładunek nie mieszka już wyłącznie w zależności. Mieszka w katalogu roboczym projektu, w plikach, których nikt rutynowo nie przegląda, i czeka na moment, w którym ktoś otworzy ten katalog w swoim środowisku. To jest przeniesienie persystencji z warstwy zależności do warstwy narzędzi, którymi deweloper pracuje — i dokładnie ta klasa, którą CrowdStrike nazwał trzy tygodnie temu, opisując robaka rejestrującego podstawiony serwer MCP w konfiguracjach asystentów.
Wtedy pisaliśmy, że dwanaście z czternastu zachowań tamtej kampanii było nieodróżnialnych od zwykłej pracy programisty, bo wykonywał je asystent AI. Ta zasada obowiązuje tu tak samo, tylko przy czterystu pakietach zamiast dziewiętnastu.
Warto odnotować, że te same hooki, ta sama nazwa setup.mjs i to samo pobieranie konkretnej wersji środowiska Bun zostały udokumentowane przez Semgrep przy kwietniowej kompromitacji pakietu w rejestrze Pythona. To nie jest więc pomysł wymyślony dziś rano — to sprawdzony moduł, który wędruje między kampaniami.
Wyzwalacz nastawiony na obrońcę
Jest w tej kampanii element, którego nie widzieliśmy dotąd w tej rodzinie i który zasługuje na osobne omówienie, bo celuje nie w ofiarę, lecz w reagującego na incydent.
Ładunek zawiera wyzwalacz uruchamiany przy rotacji poświadczeń. Rotacja to pierwszy ruch każdego zespołu po wykryciu, że wyciekły klucze — czynność tak oczywista, że w większości procedur stoi w punkcie pierwszym. Atakujący na to liczy. Zalecenie badaczy jest w związku z tym równie proste, co niepokojące w swojej treści: sprawdź, czy nikt nie obserwuje, zanim cokolwiek zrotujesz.
Drugi wyzwalacz jest wycelowany w tych, którzy niczego nie zainstalowali. Atakujący założył, że gdy kompromitacja stanie się publiczna, część deweloperów sklonuje repozytorium, żeby przeczytać kod i zobaczyć, co się stało — i przygotował dla nich osobną ścieżkę uruchomienia. To jest warte zapamiętania jako nawyk: pobranie podejrzanego repozytorium w celu obejrzenia go nie jest czynnością bierną.
Do tego dochodzi warstwa kontroli, która pokazuje, jak dojrzała jest ta rodzina malware'u. Zamiast twardo zaszytych adresów serwerów sterujących ładunek odpytuje smart kontrakt Ethereum, żeby dynamicznie pobrać aktualną domenę. Odcięcie infrastruktury przestaje być kwestią zablokowania listy adresów. Wykradzione dane trafiają natomiast do repozytoriów GitHub tworzonych pod przejętymi tożsamościami, opisanych frazą „Shai-Hulud: Here We Go Again" — takich repozytoriów założono dziś ponad pięćset.
Uczciwie: czego jeszcze nie wiemy
Kampania trwa w chwili publikacji tego tekstu i część obrazu jest niepełna. Nie ustalono publicznie, jak dokładnie przejęto konto opiekuna — ani ścieżka pierwszego dostępu, ani nazwany sprawca nie są znane. W chwili raportowania nie było też publicznego oświadczenia opiekuna, npm ani GitHuba.
Wiz przypisuje ładunek potomkowi rodziny Mini Shai-Hulud i wiąże go z wcześniejszymi kampaniami, Aikido umieszcza sierpniową aktywność w tej samej rodzinie. Nakładanie się kodu potwierdza pokrewieństwo malware'u — nie ustala jednak, kto prowadzi tę konkretną operację. To rozróżnienie warto utrzymać, bo pokusa, żeby z pokrewieństwa narzędzia wnioskować o tożsamości operatora, jest przy każdej takiej kampanii duża.
Rozbieżność liczb, o której była mowa wyżej, też jest częścią tej niepewności. Rozsądnie jest przyjąć rząd wielkości — setki pakietów, tysiące wersji — i nie przywiązywać się do konkretnej sumy, dopóki kurz nie opadnie.
Co z tego wynika dla obrońcy
Pierwsza rzecz jest natychmiastowa. Przeszukaj pliki blokad zależności, łącznie z rozwiązaniami przechodnimi, pod kątem dotkniętych pakietów i wersji — nie tylko bezpośrednich zależności, bo robak rozszedł się przez namespace'y organizacji, więc mógł trafić do twojego projektu przez pakiet, o którym nie pamiętasz. Jeśli instalowałeś cokolwiek z tej rodziny dzisiaj, zakładaj wyciek poświadczeń dostępnych dla procesu instalacji: tokenów rejestru, kluczy chmurowych, sekretów CI.
Druga wynika z hooków i jest tą, o której najłatwiej zapomnieć. Sprawdź katalogi .claude/ i .vscode/ w repozytoriach, których dotykałeś w ciągu ostatniej doby, pod kątem nieoczekiwanych plików — zwłaszcza setup.mjs i wpisów w tasks.json. Usunięcie zatrutej zależności nie usuwa tych plików, a one uruchomią się, gdy ktoś otworzy projekt i potwierdzi zaufanie do przestrzeni roboczej.
Trzecia dotyczy kolejności działań i wynika wprost z wyzwalacza na rotację. Zanim zrotujesz poświadczenia, ustal, czy na maszynie nie działa coś, co na tę rotację czeka. To odwraca standardową kolejność reagowania i warto dopisać ten krok do procedury, zanim będzie potrzebny.
Czwarta jest strategiczna i wynika z tego, że podpisy zadziałały. Skoro atestacja i provenance nie odróżniają wydania legalnego od wydania zrobionego z przejętego konta, to potrzebna jest kontrola mierząca coś innego niż pochodzenie. W praktyce oznacza to reguły reagujące na zmianę zachowania między wersjami: alert, gdy pakiet dodaje albo modyfikuje skrypt uruchamiany przy instalacji, gdy proces instalacyjny łączy się z hostem spoza rejestru, gdy opublikowana paczka rozjeżdża się z drzewem źródłowym. Badacze analizujący tę kampanię zauważają, że sama reguła wykrywająca zmianę skryptów cyklu życia wyłapałaby każdą dotychczasową falę tej rodziny.
Jedna myśl na koniec
Zbudowaliśmy wokół rejestrów pakietów warstwę zaufania, która odpowiada na pytanie „skąd to przyszło" — i odpowiada na nie dobrze. Dzisiejszy poranek pokazał, że to jest pytanie, którego nie potrzebowaliśmy. Pakiet przyszedł dokładnie stamtąd, skąd miał przyjść, przez właściwy proces, z właściwego repozytorium, wydany przez właściwe konto, z poprawnym podpisem. Wszystko się zgadzało poza jedną rzeczą, której żaden z tych mechanizmów nie mierzy: kto w tym momencie siedział po drugiej stronie konta.
To jest cena automatyzacji zaufania. Zaufanie zautomatyzowane działa z prędkością maszyny — czterysta pakietów w pół godziny — i nie zwalnia, gdy przestaje być zasłużone. Podpis nie miał okazji się pomylić. Zrobił dokładnie to, do czego go stworzyliśmy, i właśnie dlatego nikt nie zauważył.
Źródła
The Hacker News — zestawienie rozbieżnych szacunków skali z zastrzeżeniem o braku możliwości niezależnego odtworzenia szerszych sum, opis skryptu uruchamianego przy instalacji oraz hooków Claude Code i VS Code uruchamianych po zaufaniu przestrzeni roboczej, wraz z powiązaniem z kwietniową kompromitacją pakietu w rejestrze Pythona udokumentowaną przez Semgrep: https://thehackernews.com/2026/08/keyv-linked-npm-worm-poisons-hundreds.html
Aikido Security — analiza pierwotnej kompromitacji konta opiekuna, dane o skali pobrań dotkniętych bibliotek oraz opis publikacji zatrutych wersji z poprawnym provenance podpisanym przez GitHub Actions: https://www.aikido.dev/blog/keyv-and-friends-compromised-in-npm-supply-chain-attack
SafeDep — szczegółowa rekonstrukcja przebiegu: tempo przeskakiwania między organizacjami, lista dotkniętych namespace'ów, liczba repozytoriów dead-drop utworzonych tego dnia oraz opis wyzwalacza nastawionego na rotację poświadczeń i osobnego triggera dla klonujących repozytorium: https://safedep.io/keyv-npm-supply-chain-compromise/
Phoenix Security — techniczne wskazówki wykrywania, w tym polecenia do przeszukania plików blokad i katalogów .claude/ oraz .vscode/, a także propozycja reguł reagujących na zmianę skryptów cyklu życia między wersjami: https://phoenix.security/mini-shai-hulud-keyv-cacheable-npm-supply-chain-worm/
SQ Magazine — dane Wiz o tempie rozprzestrzeniania i rozmiarze ładunku, przypisanie do rodziny Mini Shai-Hulud oraz opis warstwy sterującej opartej na smart kontrakcie Ethereum: https://sqmagazine.co.uk/keyv-npm-worm-supply-chain/


























































































































































































































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ł