Shai-Hulud znów atakuje npm: keyv i setki pakietów z malware
4 sierpnia 2026 roku Aikido Security opisało trwający atak na łańcuch dostaw npm. Atakujący przejęli konto GitHub opiekuna pakietu keyv, wstrzyknęli złośliwy kod do jedenastu powiązanych bibliotek, a stamtąd robak Shai-Hulud rozszedł się dalej. Kod odpalał się sam podczas npm install, wyciągał tokeny i sekrety z komputerów deweloperów oraz runnerów CI, a następnie używał ich do publikowania kolejnych zainfekowanych paczek.
W aktualizacji z tego samego dnia, opublikowanej o 13:37 CEST, Aikido podało 434 pakiety i 1381 złośliwych wersji, łącznie ponad dwa miliardy instalacji miesięcznie. OX Security i JFrog raportowały tego dnia liczby wyższe, do około 868 pakietów. Dochodzenie trwa, więc te liczby jeszcze wzrosną.
Ta kampania jest niewygodna z prostego powodu: po drodze wszystko wyglądało poprawnie. Pakiet miał ważne poświadczenie provenance, opublikowało go prawdziwe konto opiekuna, przez prawdziwy workflow GitHub Actions. A zawierał aktywne malware. To już trzecia fala Shai-Hulud, po ataku z listopada 2025 roku i przejęciu axios w marcu, i za każdym razem wektorem jest tożsamość opiekuna, nie luka w kodzie.
Najważniejsze fakty
- Punkt wejścia: przejęte konto GitHub opiekuna pakietu
keyv, złośliwe pliki wypchnięte wprost do gałęzimain. - Uruchomienie: wpis
"preinstall": "node setup.mjs", czyli automatyczne wykonanie kodu podczasnpm install. - Payload: 728 KB w pliku
Math_Symbol.js, uruchamiany przez pobrane z GitHuba środowisko Bun. - Cel: tokeny npm i GitHub, poświadczenia AWS, Kubernetes, Vault, klucze SSH, pliki
.env, konfiguracje IDE. - Propagacja: skradzionymi tokenami robak publikował nowe wersje pakietów ofiary i dopisywał hooki do
.vscode/tasks.jsonoraz.claude/settings.json. - Eksfiltracja: dane szyfrowane RSA i wypychane do publicznych repozytoriów GitHub z opisem
Shai-Hulud: Here We Go Again.
Co się stało w ekosystemie keyv
keyv to biblioteka dająca jeden wspólny interfejs do magazynów key-value. Sama rzadko trafia do projektu świadomie, bo najczęściej przychodzi jako zależność czegoś innego. Atakujący przejęli konto GitHub jej opiekuna, dodali złośliwe pliki bezpośrednio do main i opublikowali nowe wersje w rejestrze npm.
Publikację wykonał prawidłowy workflow GitHub Actions. Dlatego paczki dostały ważne poświadczenie provenance, mimo że kod źródłowy był już zatruty. Provenance mówi, skąd wziął się artefakt i co go zbudowało. Nie mówi, czy repozytorium jest w rękach właściwej osoby.
Jedenaście pakietów zainfekowanych w pierwszej fali, z miesięczną liczbą pobrań według Aikido:
[email protected]- 604 mln pobrań miesięcznie[email protected]- 580 mln pobrań miesięcznie[email protected]- 571 mln pobrań miesięcznie[email protected]- 137 mln pobrań miesięcznie@cacheable/[email protected]- 34 mln pobrań miesięcznie[email protected]- 30 mln pobrań miesięcznie@cacheable/[email protected]- 28 mln pobrań miesięcznie[email protected]- 16 mln pobrań miesięcznie@cacheable/[email protected]- 6 mln pobrań miesięcznie@cacheable/[email protected]- 3,7 tys. pobrań miesięcznie[email protected]- 4,5 tys. pobrań miesięcznie
To nie jest lista całej kampanii, tylko jej początek. Robak używał skradzionych tokenów npm i GitHub do przejmowania paczek zupełnie innych opiekunów, więc ekspozycji nie da się ocenić po tym, czy ktoś w zespole napisał require('keyv'). Liczy się pełne drzewo zależności, razem z przechodnimi i deweloperskimi.
Jak złośliwy kod uruchamiał się podczas npm install
Do przejętych pakietów dodano dwa pliki, setup.mjs i Math_Symbol.js, oraz jeden wpis w package.json:
"preinstall": "node setup.mjs"preinstall wykonuje się automatycznie, jeszcze przed zakończeniem npm install. Nikt nie musiał niczego otwierać ani uruchamiać ręcznie. Wystarczyło zbudować projekt.
setup.mjs to silnie zaciemniony dropper. Pobierał środowisko uruchomieniowe Bun z oficjalnych releasów projektu (bun-v1.3.13) i tym Bunem odpalał właściwy payload z Math_Symbol.js. Payload ma około 728 KB i łączy trzy funkcje: kradzież poświadczeń, eksfiltrację i samodzielne rozprzestrzenianie się.
Wybór Bun nie jest przypadkowy. To legalny, podpisany plik binarny, ściągany z github.com, więc kontrole oparte na reputacji domeny albo na liście znanych złośliwych plików wykonywalnych nie mają tu czego zgłosić.
Jakie sekrety kradł Shai-Hulud
Payload jest napisany pod konkretne środowisko: stacja dewelopera, runner CI, chmura.
Tokeny npm i GitHub
Malware czytało .npmrc, zmienne środowiskowe i konfigurację GitHub CLI, szukając między innymi:
- tokenów publikacyjnych npm,
- klasycznych GitHub Personal Access Tokens,
- tokenów OAuth,
- tokenów GitHub Apps (prefiks
ghs_), - tokenów OIDC używanych przy publikacji pakietów.
Na runnerach GitHub Actions próbowało dodatkowo czytać pamięć procesu runnera, żeby wyciągnąć sekrety, które w logach pipeline'u byłyby zamaskowane.
Poświadczenia chmurowe i infrastrukturalne
Po stronie AWS payload sprawdzał pliki konfiguracyjne, zmienne środowiskowe oraz metadane instancji EC2 i kontenerów ECS, a potem próbował odpytać AWS Secrets Manager i odczytać przechowywane tam wpisy.
Poza AWS interesowały go:
- tokeny kont usługowych Kubernetes,
- pliki
KUBECONFIGi sekrety z namespace'ów, - tokeny HashiCorp Vault,
- dane logowania do rejestrów kontenerów,
- stan Terraform i pliki
tfvars.
Sekrety aplikacyjne i pliki lokalne
Skaner przechodził system plików według około 200 wzorców, pomijając pliki powyżej 5 MB. Na liście były:
- pliki
.envi.envrc, - klucze SSH i inne klucze prywatne,
- certyfikaty i keystore'y,
- tokeny Slack,
- klucze Stripe,
- connection stringi do baz danych,
- konfiguracje VPN,
- bazy KeePass,
- ustawienia VS Code i Claude Code.
Jedna instalacja na laptopie dewelopera albo na runnerze potencjalnie oddaje atakującemu naraz repozytoria, konto chmurowe, klaster i produkcję.
Jak wyglądała eksfiltracja danych
Zebrane dane były szyfrowane RSA kluczem publicznym atakującego, a potem wypychane do publicznych repozytoriów GitHub. Repozytoria używane jako punkty odbioru miały w opisie frazę Shai-Hulud: Here We Go Again. Aikido znalazło ich około 1300.
Rozwiązanie sprytne, bo ruch do github.com z runnera CI to ruch normalny. Nie odróżni go od zwykłego git push żadna reguła oparta na reputacji celu.
Jeżeli wysyłka do GitHuba się nie udała, payload przechodził na domenę npm-cache[.]com, zarejestrowaną 22 maja 2026 roku, czyli ponad dwa miesiące przed atakiem. Adresu nie miał zapisanego na stałe: pobierał go dynamicznie ze smart kontraktu Ethereum pod adresem 0xE1f2395ee43e45A1556EC6438a88c31B83493103. Operator kampanii mógł więc zmienić infrastrukturę bez publikowania nowych wersji pakietów, a zablokowanie jednej domeny niczego nie kończy.
Dlaczego to robak, a nie zwykły backdoor
Kradzież poświadczeń to była dopiero pierwsza połowa roboty.
Po znalezieniu tokenu npm malware sprawdzało, do jakich pakietów ten token ma prawo publikacji. Następnie dla każdego z nich:
- pobierało aktualną paczkę,
- podnosiło numer wersji patch,
- dopisywało złośliwy skrypt
preinstall, - wkładało payload do archiwum,
- publikowało nową wersję w npm.
Jeden npm install u opiekuna kilkudziesięciu paczek wystarczał, żeby cała ta lista wyszła do rejestru z malware w środku. Stąd skok z jedenastu pakietów do kilkuset w ciągu jednego dnia.
Drugi mechanizm szedł przez tokeny GitHub Apps. Malware commitowało zmiany do maksymalnie 50 gałęzi w repozytorium i dopisywało hooki do .claude/settings.json oraz .vscode/tasks.json. Kod odpalał się wtedy przy otwarciu repozytorium w VS Code albo przy starcie sesji Claude Code, bez żadnego npm install. Commity były podpisane jako autorstwa claude, z komunikatem chore: update config, czyli dokładnie tak, żeby przemknąć przez pobieżny przegląd historii.
Jak sprawdzić, czy atak dotyczy Waszych projektów
Aktualizacja pakietu nie jest reakcją na incydent, jeżeli złośliwa wersja już się wykonała. W takiej sytuacji przyjmijcie założenie, że wszystko, co było dostępne na tej maszynie, wyciekło.
1. Przeszukaj całe drzewo zależności
Przejrzyj:
package-lock.json,npm-shrinkwrap.json,yarn.lock,pnpm-lock.yaml,- wygenerowane SBOM-y,
- cache menedżerów pakietów,
- logi instalacji i logi pipeline'ów.
Zależności bezpośrednie to najmniejszy problem. Sprawdźcie przechodnie i devDependencies, bo pakiet używany tylko do lintowania wykonuje skrypt instalacyjny dokładnie tak samo jak zależność produkcyjna. file-entry-cache siedzi w drzewie ESLinta w bardzo dużej liczbie projektów.
2. Zatrzymaj dotknięte pipeline'y
Jeżeli złośliwa wersja weszła, wstrzymajcie powiązane pipeline'y i odizolujcie runnery. Kolejne buildy przed wyczyszczeniem środowiska tylko dokładają wycieków i mogą wpuścić payload do następnych artefaktów.
Przy runnerach efemerycznych maszyna już nie istnieje, więc sprawdźcie to, co da się sprawdzić: jakie sekrety, uprawnienia i relacje zaufania były podpięte do zainfekowanego joba.
3. Wymień zagrożone poświadczenia
Kolejność ma znaczenie, więc od najpilniejszych:
- tokeny npm,
- GitHub PAT, OAuth i GitHub Apps,
- klucze AWS i pozostałe poświadczenia chmurowe,
- tokeny Kubernetes i Vault,
- klucze SSH,
- sekrety aplikacyjne z plików
.env, - tokeny Slack i klucze Stripe,
- dane logowania do rejestrów kontenerów.
Rotacja jednego tokenu npm nic nie da. Payload przeszukiwał około 200 lokalizacji i zabrał wszystko, do czego miał dostęp w danym środowisku.
4. Przejrzyj repozytoria i logi audytowe
Szukajcie:
- commitów autorstwa
claude, których nikt nie zna, - komunikatu
chore: update config, - zmian w
.claude/settings.jsoni.vscode/tasks.json, - wersji pakietów opublikowanych bez wiedzy zespołu,
- nowych publicznych repozytoriów na koncie organizacji,
- nietypowych operacji wykonanych przez GitHub Apps,
- wywołań AWS Secrets Manager,
- odczytów sekretów Kubernetes i Vault.
Materiał jest rozrzucony po logach GitHuba, npm, chmury, systemu CI i menedżera sekretów. Warto przejść je równolegle, bo ślad w jednym miejscu zwykle tłumaczy anomalię w drugim.
5. Odbuduj środowisko od zera
Po usunięciu złośliwych zależności:
- odtwórzcie lockfile na podstawie zweryfikowanych wersji,
- wyczyśćcie cache pakietów,
- uruchomcie build w świeżym środowisku,
- przebudujcie obrazy kontenerowe i artefakty,
- sprawdźcie, czy zainfekowane pliki nie trafiły do repozytoriów albo obrazów,
- porównajcie wynik z wcześniejszym, zaufanym buildem.
Wskaźniki kompromitacji
Aikido opublikowało następujące IoC:
- Dropper:
setup.mjs, SHA-25654dc7ea54a1317cca0e890a2770630cf7fa6c97813e0cb9d2caa93012b350668 - Wariant rozprzestrzeniający, SHA-256
fd3ca4007b225fdf8de7af4345a19179d5efa8c4bb9205f88cda806e5684b1eb - Payload:
Math_Symbol.jsorazmath_init.js, SHA-2569fc2570b7cef51c1b8df116d144d11ff4096357be7d2c4c6367cfc2509cf1bcc - Endpoint zapasowy:
npm-cache[.]com:443/router - RPC Ethereum:
eth-mainnet.nodereal.io - Smart kontrakt C2:
0xE1f2395ee43e45A1556EC6438a88c31B83493103 - Repozytoria GitHub z opisem
Shai-Hulud: Here We Go Again
Te wskaźniki przydają się w dochodzeniu, ale brak trafień nie jest dowodem, że nic się nie stało. Infrastruktura rotuje się przez smart kontrakt, warianty payloadu mają różne hashe, a spora część aktywności biegnie przez GitHuba, czyli usługę, którą i tak macie na allowliście.
Czy lockfile, SCA i provenance chronią przed takim atakiem
Każdy z tych mechanizmów robi coś sensownego. Żaden nie zatrzymałby tej kampanii sam.
Lockfile pilnuje, żeby wersje nie zmieniały się bez wiedzy zespołu. Nie pomoże, kiedy wskazuje już na wersję złośliwą.
Klasyczne SCA pyta o CVE. Świeżo opublikowane malware nie ma CVE i mieć nie musi, bo problemem nie jest błąd do wykorzystania, tylko kod dopisany celowo. Skaner podatności zobaczy tu czysty pakiet.
Trusted Publishing i provenance są dobrym kierunkiem: mniej długowiecznych tokenów publikacyjnych, weryfikowalne źródło artefaktu. GitHub idzie dalej i rekomenduje WebAuthn, wymuszone 2FA przy publikacji oraz wycofanie klasycznych tokenów npm. To wszystko ma sens i to wszystko obeszła jedna sytuacja: atakujący przejął konto opiekuna, zmienił kod w repozytorium i pozwolił poprawnemu workflow zrobić resztę.
Sensowna ochrona składa się więc z kilku warstw jednocześnie:
- kontrola kont i procesów publikacyjnych,
- ochrona gałęzi i workflow CI/CD,
- minimalne uprawnienia tokenów,
- analiza lockfile'ów i SBOM,
- wykrywanie malware w zależnościach,
- weryfikacja pakietu przed instalacją,
- monitoring stacji deweloperskich i runnerów.
Jak Aikido wykrywa złośliwe pakiety
Aikido analizuje lockfile'e i same paczki, a jego mechanizm malware detection patrzy na zachowanie, nie na listę CVE. Zgłasza między innymi:
- silną obfuskację kodu,
- wykonywanie poleceń podczas instalacji,
- nieoczekiwaną komunikację z zewnętrznymi serwerami,
- próby eksfiltracji danych,
- pobieranie i odpalanie kolejnych payloadów.
Tę kampanię Aikido klasyfikuje jako problem krytyczny z wynikiem 100/100. Użytkownikom platformy zaleca sprawdzenie centralnego feedu, odfiltrowanie problemów typu malware i ręczne uruchomienie skanowania, bez czekania na nocny cykl.
Osobną warstwą jest Aikido Safe Chain. Narzędzie przechwytuje polecenia npm, npx, yarn, pnpm i pnpx, sprawdza pakiet w Aikido Intel i dopiero potem pozwala na instalację. Działa na stacjach deweloperskich i w pipeline'ach, czyli w obu miejscach, w których wykonywał się preinstall.
Aikido Device Protection dodaje widoczność na to, co poza zależnościami aplikacji ląduje na sprzęcie zespołu: rozszerzenia IDE, dodatki przeglądarek, pakiety instalowane lokalnie. W kampanii, która dopisuje hooki do .vscode/tasks.json, ta warstwa przestaje być opcjonalna.
Jak ICWT pomaga zabezpieczyć łańcuch dostaw
Jako partner Aikido w Polsce wdrażamy platformę tak, żeby objęła cały tor: kod, pipeline i sprzęt deweloperów.
Typowy zakres takiego wdrożenia:
- podłączenie repozytoriów i konfiguracja skanowania zależności,
- namierzenie projektów, które ciągną zagrożone wersje bezpośrednio albo przechodnio,
- uruchomienie i dostrojenie detekcji malware, żeby nie utonęła w szumie,
- Aikido Safe Chain na stacjach deweloperskich i w CI/CD,
- przegląd zarządzania tokenami npm, GitHub i poświadczeniami chmurowymi oraz ograniczenie uprawnień workflow,
- wpięcie wyników Aikido w proces obsługi incydentów i zasady dopuszczania nowych zależności open source.
Chodzi o skrócenie czasu między publikacją złośliwego pakietu a jego wykryciem, i o zatrzymanie zależności, zanim wykona się na laptopie albo w pipeline. Jeden dodatkowy skaner produkujący alerty niczego tu nie zmienia.
Jeżeli chcecie sprawdzić, czy zagrożone wersje siedzą w Waszych repozytoriach i środowiskach CI/CD, napiszcie do nas. Pomożemy ocenić ekspozycję i zaplanować ochronę łańcucha dostaw na Aikido.
Źródła
- Aikido Security, "Keyv and friends compromised in active Shai-Hulud supply chain attack"
- OX Security, "A Massive Shai-Hulud Campaign Hits npm"
- JFrog Security Research, "Major Shai-Hulud campaign strikes npm again"
- GitHub Security, "Our plan for a more secure npm supply chain"
- Dokumentacja Aikido: malware detection oraz Safe Chain.
Najczęściej zadawane pytania
Czy jesteśmy bezpieczni, jeżeli nie używamy pakietu keyv bezpośrednio?
Niekoniecznie. keyv i cacheable trafiają do projektów jako zależności przechodnie, na przykład przez file-entry-cache w konfiguracji ESLint. Do tego robak infekował kolejne pakiety innych opiekunów przy użyciu skradzionych tokenów, więc lista zagrożonych paczek jest znacznie szersza niż pierwotne jedenaście. Sprawdzajcie pełny lockfile, a nie wpisy w package.json.
Czy wystarczy zaktualizować zainfekowany pakiet?
Nie, jeżeli złośliwa wersja została już zainstalowana. Skrypt preinstall wykonywał się przed zakończeniem npm install i mógł wtedy wykraść sekrety. Aktualizacja usuwa problem z przyszłych buildów, ale nie unieważnia poświadczeń, które już wyciekły. Trzeba je wymienić.
Czy ważne provenance oznacza, że pakiet jest bezpieczny?
Nie. Provenance potwierdza, z jakiego repozytorium i przez jaki workflow powstał artefakt. W tej kampanii atakujący zmienił kod w prawdziwym repozytorium, a potem pozwolił zwykłemu workflow GitHub Actions opublikować paczkę. Provenance było poprawne, a pakiet zawierał malware.
Czy skaner CVE wykryje taki atak?
Nie zawsze. Świeżo opublikowane malware nie ma identyfikatora CVE, bo problemem nie jest błąd w kodzie, tylko kod dopisany celowo. Potrzebna jest osobna analiza pakietów: obfuskacja, skrypty instalacyjne, ruch sieciowy podczas instalacji.
Czy wyłączenie skryptów instalacyjnych rozwiązuje problem?
Ogranicza ryzyko, ale nie zamyka sprawy. Część legalnych pakietów potrzebuje skryptów instalacyjnych, a ta kampania odpalała się także z .vscode/tasks.json i .claude/settings.json, całkowicie poza npm install. Flaga --ignore-scripts jest jedną z warstw, nie całą obroną.
Ile pakietów npm zostało zainfekowanych?
Aikido potwierdziło 434 pakiety i 1381 złośliwych wersji w aktualizacji z 4 sierpnia 2026 o 13:37 CEST. Inni badacze, w tym OX Security i JFrog, raportowali w tym samym dniu wyższe liczby, do około 868 pakietów. Dochodzenie nadal trwa, więc ostateczna liczba będzie większa.
Powiązane artykuły
Ewolucja Zagrożenia: Dlaczego Shai-Hulud 2.0 zmusza nas do ponownego zdefiniowania Bezpieczeństwa Łańcucha Dostaw Oprogramowania
Analiza ataku Shai-Hulud 2.0 na ekosystem npm — 796 skompromitowanych pakietów, dead man's switch i jak OX Security chroni łańcuch dostaw oprogramowania.
Cloudflare AI Gateway: logi, DLP i kontrola kosztów AI
Cloudflare AI Gateway centralizuje ruch aplikacji, agentów i automatyzacji do modeli AI. Pokazujemy, jak wykorzystać logi, DLP, Guardrails, cache i limity kosztów - oraz czego ta usługa nie obejmuje.
SAST i DAST: Kompletny przewodnik po nowoczesnym bezpieczeństwie aplikacji
SAST vs DAST - porównanie metod testowania bezpieczeństwa aplikacji. Tabela różnic, kluczowe korzyści i wskazówki wdrożenia dla zespołów DevSecOps.