Przejdź do treści głównej

Shai-Hulud znów atakuje npm: keyv i setki pakietów z malware

Sebastian CTO
10 min czytania
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łęzi main.
  • Uruchomienie: wpis "preinstall": "node setup.mjs", czyli automatyczne wykonanie kodu podczas npm 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.json oraz .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:

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 KUBECONFIG i 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 .env i .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:

  1. pobierało aktualną paczkę,
  2. podnosiło numer wersji patch,
  3. dopisywało złośliwy skrypt preinstall,
  4. wkładało payload do archiwum,
  5. 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.json i .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-256 54dc7ea54a1317cca0e890a2770630cf7fa6c97813e0cb9d2caa93012b350668
  • Wariant rozprzestrzeniający, SHA-256 fd3ca4007b225fdf8de7af4345a19179d5efa8c4bb9205f88cda806e5684b1eb
  • Payload: Math_Symbol.js oraz math_init.js, SHA-256 9fc2570b7cef51c1b8df116d144d11ff4096357be7d2c4c6367cfc2509cf1bcc
  • 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

Tagi:
#Aikido #npm #keyv #Shai-Hulud #Supply Chain Security #DevSecOps #Malware #CI/CD

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.