Shai-Hulud to samorozprzestrzeniający się robak infekujący pakiety npm, czyli atak na łańcuch dostaw oprogramowania, który każdą ofiarę angażuje do dalszego rozprzestrzeniania. Uruchamia się przy instalacji pakietu, kradnie z maszyny tokeny npm, tokeny GitHub i klucze chmurowe, a następnie używa tokenu npm do publikowania zainfekowanych wersji innych pakietów ofiary. Od pierwszej fali we wrześniu 2025 roku wielokrotnie powracał, a w maju 2026 roku jego autorzy opublikowali kod źródłowy, więc każda kolejna fala może być dziełem innej grupy. Jeśli którykolwiek z Państwa buildów zainstalował zainfekowaną wersję, należy traktować to zadanie jako przejęte i zrotować każdy sekret, który mogło odczytać.
Największe szkody powstają w CI. W fali z listopada 2025 roku firma Wiz ustaliła, że tylko 23% infekcji dotyczyło maszyn programistów, a pozostałe wystąpiły głównie w pipeline'ach CI/CD, najczęściej w GitHub Actions. Build instaluje setki pakietów, przechowuje sekrety potrzebne do publikacji i wdrożeń oraz uruchamia każdy skrypt instalacyjny bez niczyjego nadzoru. Dlatego gdy zostaje ogłoszona nowa fala, każdy zespół zadaje to samo pytanie: czy któryś z naszych buildów zainstalował zainfekowaną wersję?
Co się wydarzyło
| Kiedy | Fala | Co było inne |
|---|---|---|
| Wrzesień 2025 | Shai-Hulud | Skrypt postinstall uruchamiał bundle.js o rozmiarze 3,6 MB, używał TruffleHog do wyszukiwania sekretów i ponownie publikował do 20 pakietów opiekuna. Dotkniętych zostało ponad 500 pakietów, w tym @ctrl/tinycolor i 18 pakietów @crowdstrike. CISA opublikowała alert 23 września. |
| Listopad 2025 | Sha1-Hulud: The Second Coming | Przejście na preinstall, instalacja środowiska Bun do uruchomienia ładunku o rozmiarze 10 MB, tylne furtki w maksymalnie 100 pakietach na opiekuna i rejestrowanie zainfekowanych maszyn jako runnerów self-hosted GitHub. Ucierpiały pakiety Zapier, ENS, PostHog i Postman; Wiz naliczył ponad 25 000 repozytoriów z ujawnionymi sekretami. |
| Grudzień 2025 | Test „3.0” | Zmodyfikowana wersja w jednym pakiecie, bez istotnego rozprzestrzeniania. |
| Maj 2026 | TanStack | 84 złośliwe wersje 42 pakietów @tanstack. Atakujący nigdy nie ukradł tokenu npm: workflow pull_request_target, zatruty cache Actions i token OIDC odczytany z pamięci runnera pozwoliły mu publikować przez własny trusted publishing TanStack. Śledzone jako CVE-2026-45321, dodane do katalogu KEV CISA 27 maja. |
| Maj 2026 | AntV | 639 wersji 323 pakietów. Na linuksowych runnerach GitHub Actions ładunek odczytywał sekrety z pamięci procesu runnera, omijając maskowanie w logach. GitHub unieważnił 61 274 tokeny npm z prawem zapisu. |
| Czerwiec 2026 | Miasma | 32 pakiety @redhat-cloud-services, opublikowane przez wstrzyknięte workflowy z ważnymi danymi o pochodzeniu. Każda infekcja niosła unikatowo zaszyfrowany ładunek, więc wykrywanie oparte na hashach zawiodło. |
| Sierpień 2026 | CHAINDROP | Popularne pakiety cache, takie jak keyv, cacheable i cache-manager, a łącznie setki dotkniętych pakietów. Utrwalał się przez pliki konfiguracyjne edytorów i asystentów AI w repozytorium. |
| Wrzesień 2026 | Powrót | 7 września ładunek powrócił w czterech pakietach, które przeszły przez nowe skanowanie złośliwego oprogramowania w npm na etapie publikacji. |
| Październik 2026 | tensorlake | 8 października opublikowano [email protected] z robakiem. OX Security zauważył nowe klucze atakujących i sugeruje „niezależnego aktora zagrożeń lub nową grupę posługującą się nazwą Shai-Hulud”. |
Co robak robi w zadaniu CI
Szczegóły zmieniają się między falami, ale wzorzec pozostaje taki sam:
- Działa przy instalacji. Skrypt
preinstalllubpostinstallwykonuje się, gdy tylkonpm installlubnpm cidotrze do zainfekowanej wersji. Nic w Państwa kodzie nie musi jej importować. - Zbiera sekrety. Zmienne środowiskowe, tokeny
.npmrc, poświadczenia chmurowe z usługi metadanych i menedżerów sekretów oraz wszystko, co TruffleHog znajdzie na dysku. Późniejsze fale odczytywały bezpośrednio pamięć runnera GitHub Actions, zdobywając sekrety, które nigdy nie zostały udostępnione danemu krokowi i które maskowanie ukryłoby w logach. - Wyprowadza je. Wczesne fale tworzyły publiczne repozytoria GitHub nazwane od robaka i wypychały workflow wysyłający wszystkie sekrety repozytorium do
webhook.site. Późniejsze fale korzystały z serwerów atakujących i sieci komunikacyjnych. - Rozprzestrzenia się i zostaje. Za pomocą skradzionego tokenu npm publikuje zainfekowane wersje innych pakietów. Za pomocą tokenu GitHub dodaje workflowy, rejestruje runnery self-hosted lub podkłada pliki konfiguracyjne, które uruchamiają się przy następnym otwarciu repozytorium.
- W CI zachowuje się inaczej. Ładunek z listopada 2025 roku sprawdzał zmienne
GITHUB_ACTIONS,BUILDKITE,CIRCLE_SHA1i podobne, a w CI działał synchronicznie, kończąc pracę, zanim build przeszedł dalej.
Kogo to dotyczy
Każdego, czyje maszyny lub buildy zainstalowały zainfekowaną wersję, gdy była dostępna w rejestrze: laptopy programistów, zadania CI, buildy Docker uruchamiające npm install i runnery self-hosted. Nie trzeba zależeć od pakietu bezpośrednio; wystarczy zależność przechodnia lub narzędzie uruchomione przez npx. Każda fala ma własną listę pakietów i wersji, a zainfekowane wersje są zwykle usuwane w ciągu kilku godzin lub dni, więc liczy się to, czy coś zainstalowało je w tym okresie.
Jak to działa
Shai-Hulud nie jest błędem w npm. To złośliwy kod opublikowany w pakietach, które poza tym są legalne; biuletyny GitHub o złośliwym oprogramowaniu w zainfekowanych pakietach klasyfikują go jako osadzony złośliwy kod (CWE-506). Każda fala łączy kilka technik:
- Wykonanie podczas instalacji. npm uruchamia podczas instalacji skrypty cyklu życia pakietu, takie jak
preinstall,postinstalliprepare, z uprawnieniami użytkownika lub zadania CI. Zainfekowana wersja musi się tylko znaleźć w drzewie zależności; nic nie musi jej importować. - Rozprzestrzenianie się dzięki skradzionym uprawnieniom do publikacji. Z tokenem npm lub GitHub odebranym ofierze robak publikuje nowe wersje innych pakietów tej ofiary z dodanym ładunkiem, więc każdy zainfekowany opiekun infekuje kolejne pakiety.
- Publikacja przez własny pipeline wydań ofiary (TanStack, maj 2026). Według biuletynu GitHub atakujący połączył workflow
pull_request_target, cache GitHub Actions zatruty ponad granicą zaufania między forkiem a repozytorium bazowym oraz token OIDC odczytany z pamięci runnera i opublikował pakiety przez trusted publishing TanStack bez zmiany workflow publikacji. Zainfekowane wersje deklarowały opcjonalną zależność od commita w forku, którego skryptprepareuruchamiał ładunek, a potem celowo kończył się błędem, więc npm ją odrzucał i nie zostawał żaden ślad wnode_modules. - Prawidłowe dane o pochodzeniu (Miasma, czerwiec 2026). Wstrzyknięte workflowy publikowały z własnych repozytoriów ofiar, więc zainfekowane wersje miały prawidłowe dane o pochodzeniu, a ładunek szyfrowany unikalnie dla każdej infekcji udaremniał wykrywanie oparte na hashach.
Jak sprawdzić, czy to Państwa dotyczy
- Zdobyć listę dotkniętych wersji dla danej fali. Dostawcy bezpieczeństwa publikują ją w ciągu kilku godzin; Datadog prowadzi na przykład repozytorium wskaźników kompromitacji z listami pakietów i wersji.
- Przeszukać każdy lockfile na każdej gałęzi, na której w tym okresie działało CI, a nie tylko na domyślnej.
- Przeszukać same buildy. Szukać w logach CI plików
setup_bun.js,bun_environment.jslub innych nazw wymienionych w biuletynie oraz wyników instalacji wspominających dotknięte wersje. Jeśli przechowują Państwo SBOM-y dla każdego buildu, można przeszukać je pod kątem tych wersji. - Sprawdzić organizację GitHub pod kątem nowych repozytoriów nazwanych od robaka, plików workflow, których Państwo nie napisali (na przykład
shai-hulud-workflow.yml,discussion.yamllubformatter_*.yml), oraz runnerów self-hosted, których Państwo nie zarejestrowali.
Polecenia do dostosowania. Aby przeszukać lockfile'e wszystkich zdalnych gałęzi, należy umieścić dotknięte wersje z danej fali w affected.txt, po jednej name@version w wierszu:
git fetch --all --quiet
for ref in $(git for-each-ref --format='%(refname:short)' refs/remotes); do
while read -r pkg; do
name=${pkg%@*}; version=${pkg##*@}
git grep -n -A3 -F "$name" "$ref" -- '*package-lock.json' '*yarn.lock' '*pnpm-lock.yaml' \
| grep -F "$version" | sed "s|^|$pkg |"
done < affected.txt
done
Aby wyświetlić runnery self-hosted organizacji, nowe repozytoria wspominające robaka i pliki workflow, o których wiadomo, że tworzy je robak (zastąpić ORG):
gh api --paginate orgs/ORG/actions/runners --jq '.runners[] | [.name, .os, .status] | @tsv'
gh repo list ORG --limit 1000 --json name,description,createdAt \
--jq '.[] | select((.description // "") | test("hulud"; "i")) | [.name, .createdAt] | @tsv'
for repo in $(gh repo list ORG --limit 1000 --json name --jq '.[].name'); do
gh api "repos/ORG/$repo/contents/.github/workflows" --jq '.[].name' 2>/dev/null \
| grep -E '^(shai-hulud-workflow\.yml|discussion\.yaml|formatter_.*\.yml)$' \
| sed "s|^|$repo |"
done
Repozytoria mogą też mieć własne runnery (gh api repos/ORG/REPO/actions/runners), a wczesne fale tworzyły repozytoria na prywatnych kontach programistów, którym ukradły tokeny, dlatego warto poprosić programistów o sprawdzenie swoich kont.
Dlaczego lockfile tego nie rozstrzyga
Standardowa rada to przeszukać package-lock.json, yarn.lock lub pnpm-lock.yaml pod kątem dotkniętych wersji. Warto zrobić to najpierw, ale czysty lockfile nie dowodzi czystego buildu:
- Pakiety instalowane poza lockfile.
npx some-toolinstaluje pakiet niebędący zależnością projektu do cache npm.npm install -gw kroku konfiguracji, narzędzie instalowane w skrypcie buildu inpm installw Dockerfile omijają lockfile projektu. - Dzisiejszy lockfile na gałęzi to nie ten, który został użyty. Build sprzed trzech tygodni użył tego, co lockfile mówił wtedy, na gałęzi, która go uruchomiła. Pull requesty z aktualizacjami zależności uruchamiają CI na nowych wersjach, zanim ktokolwiek je przejrzy.
- Cache przywraca to, co zainstalowano wcześniej. Przywrócone
node_moduleslub cache npm mogą zawierać wersję, o której bieżący lockfile już nie wspomina. npm ciinstaluje to, co mówi lockfile. Jeśli zainfekowana wersja była już przypięta w lockfile, ścisła instalacja wiernie ją zainstaluje.- Poprawnie opublikowana wersja nadal może być złośliwa. TanStack i Miasma zostały przejęte przez własne workflowy wydań, a zainfekowane wersje Miasma miały ważne dane o pochodzeniu. Sprawdzenie, że wersja została poprawnie opublikowana, nie mówi, że jest czysta.
Tak naprawdę potrzebny jest zapis tego, jakie wersje pakietów pobrał każdy build, we wszystkich repozytoriach i na wszystkich gałęziach, przez cały okres, w którym zainfekowana wersja była w rejestrze. Więcej w artykule czy to nas dotyczy? Jak ocenić nowy CVE we wszystkich buildach.
Jak to naprawić i co zrobić, jeśli nie można jeszcze załatać
- Jeśli jakikolwiek build zainstalował dotkniętą wersję, traktować zadanie jako przejęte. Zrotować każdy sekret, który mogło odczytać: tokeny npm, tokeny GitHub, poświadczenia chmurowe i klucze wdrożeniowe. Ponieważ późniejsze fale publikowały skradzione sekrety na kontach innych ofiar, nie należy zakładać bezpieczeństwa tylko dlatego, że nic nie pojawiło się na Państwa własnych kontach.
- Wyczyścić cache. Usunąć cache GitHub Actions i cache npm na maszynach programistów i runnerach self-hosted, aby następny build zainstalował czyste wersje:
gh cache delete --all --repo ORG/REPO # GitHub Actions caches, per repository
npm cache clean --force
rm -rf node_modules "$(pnpm store path)" # pnpm; for Yarn: yarn cache clean
- Przypiąć czystą wersję i usunąć workflowy, repozytoria, runnery i pliki konfiguracyjne dodane przez robaka.
Weryfikacja czyszczenia
- Każdy lockfile wskazuje czyste wersje. Na każdej gałęzi
npm ls <package>(lubpnpm whyalboyarn why) pokazuje zainstalowaną wersję; żadna nie może być na liście z danej fali. - Czysta instalacja nie zawiera plików ładunku. Po opisanym wyżej wyczyszczeniu cache należy zainstalować pakiety ponownie i poszukać plików ładunku wymienionych w biuletynach:
find node_modules -name bundle.js -size +3M \
-o -name setup_bun.js -o -name bun_environment.js -o -name router_init.js
- Stare poświadczenia już nie działają. Unieważnione tokeny npm zniknęły z
npm token list, stare tokeny GitHub i klucze wdrożeniowe są usunięte, a klucze chmurowe dezaktywowane, a nie tylko zastąpione. - Cache zniknęły.
gh cache listnie pokazuje w żadnym dotkniętym repozytorium cache utworzonego przed czyszczeniem. - Nie zostało nic, co dodał robak: żadne repozytoria, pliki workflow, runnery self-hosted ani pliki konfiguracyjne edytorów i asystentów AI, których Państwo nie utworzyli.
- Następny build jest czysty. Sprawdzić wersje, które faktycznie zainstalował, na podstawie jego logu lub SBOM buildu, a nie tylko lockfile'a.
Jak ograniczyć szkody następnym razem
- Wyłączyć skrypty instalacyjne, gdzie to możliwe. npm v12, wydany w lipcu 2026 roku, domyślnie wyłącza skrypty instalacyjne i pozwala dopuszczać je dla poszczególnych pakietów. pnpm i Yarn oferują podobne mechanizmy.
- Odczekać przed przyjęciem nowych wersji. Większość zainfekowanych wersji jest wykrywana i usuwana w ciągu kilku godzin lub dni. Okres karencji dla aktualizacji zależności, obsługiwany zarówno przez Dependabota, jak i Renovate, trzyma je z dala od Państwa buildów. Package firewall GitHub, od 20 października 2026 roku dostępny w publicznej wersji zapoznawczej dla GitHub Enterprise Cloud, sprawdza instalacje npm pod kątem złośliwych pakietów i również oferuje okresy karencji.
- Publikować przez trusted publishing i krótkotrwałe tokeny. npm unieważnił wszystkie klasyczne tokeny w grudniu 2025 roku. Zamiast długotrwałych tokenów należy używać opartego na OIDC trusted publishing z CI, a workflowy publikujące trzymać oddzielnie od workflowów buildu i testów.
- Wzmocnić workflowy nadużywane przez robaka. Unikać
pull_request_target, przypinać akcje do SHA commitów i dawaćGITHUB_TOKENtylko te uprawnienia, których potrzebuje każde zadanie. Więcej w artykule Sekrety GitHub Actions: jak wyciekają i jak je zabezpieczyć. - Ograniczyć, dokąd buildy mogą się łączyć. Build potrzebuje swoich rejestrów pakietów i kilku API. Jeśli może sięgać tylko tam, wywołania robaka do
webhook.sitelub jego własnych serwerów się nie powiodą. Więcej w artykule jak polityka egress zatrzymuje eksfiltrację z GitHub Actions. Nie zatrzymuje to eksfiltracji przez host, z którego build legalnie korzysta, takiego jak własne API GitHub, na którym polegały niektóre fale.
Jak sprawdzić, czy doszło do włamania
Shai-Hulud jest złośliwy z założenia: każda maszyna lub zadanie, które zainstalowało zainfekowaną wersję, uruchomiło go. Wskaźniki różnią się w zależności od fali, dlatego należy korzystać z biuletynu dla sprawdzanej fali. Ze źródeł pierwotnych:
| Fala | Wskaźniki | Źródło |
|---|---|---|
| Wrzesień 2025 | Ładunek bundle.js, SHA-256 46faab8ab153fae6e80e7cca38eab363075bb524edd79e42269217a083628f09; workflow .github/workflows/shai-hulud-workflow.yml na gałęzi shai-hulud; publiczne repozytorium o nazwie Shai-Hulud; połączenia z webhook.site | StepSecurity, CISA |
| Listopad 2025 | setup_bun.js, SHA-1 d1829b4708126dcc7bea7437c04d1f10eacd4a16; bun_environment.js, SHA-1 d60ec97eea19fffb4809bc35b91033b52490ca11 lub 3d7570d14d34b0ba137d502f042b27b0f37a59fa; repozytoria z plikami cloud.json, contents.json, environment.json, truffleSecrets.json i actionsSecrets.json; workflowy .github/workflows/discussion.yaml i formatter_*.yml; runner self-hosted o nazwie SHA1HULUD | Wiz |
| Maj 2026, TanStack | "@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c" w optionalDependencies; ładunek router_init.js (ok. 2,3 MB) w katalogu głównym pakietu; skrypt pomocniczy tanstack_runner.js; połączenia z filev2.getsession.org oraz od seed1.getsession.org do seed3.getsession.org, a także pobrania drugiego etapu z litter.catbox.moe | GHSA-g7cv-rxg3-hmpx |
Późniejsze fale zmieniały ładunek przy każdej infekcji, więc niezgodny hash pliku niczego nie dowodzi; najpierw należy dopasowywać wersje pakietów.
Kiedy założyć włamanie. Każdy host i każde zadanie CI, które zainstalowało dotkniętą wersję, należy traktować jako przejęte, nawet jeśli instalacja się nie powiodła lub nie zostawiła niczego w node_modules. Biuletyn GitHub dotyczący TanStack stwierdza, że każde środowisko programistyczne lub CI, w którym zainstalowano dotkniętą wersję, „należy uznać za przejęte”. Sprawdzać należy od pierwszej publikacji zainfekowanej wersji, a nie od dnia ogłoszenia fali.
Zrotować poświadczenia. Zrotować wszystko, do czego instalacja miała dostęp. W fali TanStack oznaczało to poświadczenia AWS, GCP, Kubernetes, Vault, GitHub, npm i SSH, według postmortemu. Przejrzeć logi audytowe chmury pod kątem aktywności z dotkniętych hostów w trakcie okna instalacji i po nim.
Wyczyścić cache i artefakty. Przeszukać repozytoria artefaktów i proxy zależności pod kątem kopii dotkniętych wersji w cache i usunąć je, zgodnie z zaleceniem CISA. Usunąć node_modules, cache npm i cache CI; w fali TanStack drogą wejścia atakującego był zatruty cache magazynu pnpm, dlatego należy sprawdzić cache Actions, a także cache pakietów. Przebudować i ponownie wydać wszystko, co wyprodukowało przejęte zadanie.
Co powiedzieć klientom
Jeśli dostarczają Państwo oprogramowanie budowane z użyciem npm, klienci zapytają, czy Państwa produkt zawiera robaka lub czy Państwa pipeline wydań został przejęty. Proszę odpowiedzieć, co sprawdzono i co zrobiono:
Sprawdziliśmy każdy build [produkt] od [data] do [data] względem opublikowanych list wersji pakietów npm zainfekowanych przez Shai-Hulud. [Żaden build nie zainstalował dotkniętej wersji.] / [Buildy [komponent] z [data] zainstalowały [pakiet@wersja]; zrotowaliśmy poświadczenia, do których te buildy miały dostęp, przebudowaliśmy [produkt] z czystych zależności i wydaliśmy [wersja]. Wydania od [x] do [y] są dotknięte; prosimy o aktualizację do [wersja].] Zaktualizujemy ten komunikat, jeśli nowe fale zmienią sytuację.
Jeśli sprawdzanie wciąż trwa, proszę to powiedzieć i podać, kiedy przyjdzie odpowiedź. Pierwsze oświadczenie i szablony na każdy wynik znajdą Państwo w tekście jak odpowiadać klientom po ataku na łańcuch dostaw.
Jak pomaga CRACI
CRACI uruchamia Państwa zadania GitHub Actions na własnych runnerach (runs-on: craci) i rejestruje każdy pakiet, który każde zadanie faktycznie pobiera z npm i innych rejestrów, łącznie z pakietami przywróconymi z cache. Ten zapis obejmuje opisane wyżej martwe pola: narzędzia npx, instalacje globalne i instalacje w skryptach buildu pojawiają się w nim, ponieważ pochodzi on z tego, co zadanie pobrało, a nie z tego, co zadeklarował lockfile. Gdy zostaje ogłoszona nowa fala, wyszukują Państwo dotknięte wersje w buildach zarejestrowanych przez CRACI, zamiast odtwarzać je z lockfile i logów, a ślad sieciowy każdego zadania pokazuje, z czym się łączyło. Historia buildów przechowuje każdy wcześniejszy build z jego SBOM, dzięki czemu widzą Państwo, które buildy zawierały zainfekowaną wersję i w jakim okresie byli Państwo narażeni, w ramach okresu przechowywania danych.
Polityka egress CRACI ogranicza, dokąd zadanie może się łączyć, jest walidowana przed startem zadania, w razie awarii blokuje ruch (fail closed) i wysyła alert, gdy zadanie próbuje połączyć się z celem niedozwolonym przez politykę. Zainfekowane wydania z opublikowanym CVE, takie jak fala TanStack, są oznaczane jak każda inna podatność, a bramki polityk mogą blokować buildy, które je zawierają. CRACI nie ocenia pakietów pod kątem złośliwego zachowania i nie oznacza nowej zainfekowanej wersji, zanim pojawi się biuletyn; aby blokować zainfekowane wersje przed instalacją, warto połączyć CRACI z narzędziem do oceny reputacji pakietów.
Zobacz zapis pakietów jednego z własnych buildów
Inne incydenty: wszystkie podatności.
Źródła i aktualizacje
Źródła pierwotne i oryginalne badania:
- CISA: alert z 23 września 2025 roku, katalog KEV
- GitHub: GHSA-g7cv-rxg3-hmpx (CVE-2026-45321), plan bezpieczniejszego łańcucha dostaw npm, unieważnienie klasycznych tokenów npm
- Postmortem TanStack
- Wiz o fali z listopada 2025 roku i jej następstwach
- Microsoft o AntV i wskazówki Microsoftu dotyczące wykrywania
- Datadog i Elastic o CHAINDROP
- StepSecurity o fali z września 2025 roku
- FIRST: ocena EPSS dla CVE-2026-45321
Aktualizacje:
- 2026-11-11: Dodano do ramki EPSS, klasę CWE i podziękowanie, gotowe polecenia do skopiowania oraz sekcje o tym, jak działa robak, jak zweryfikować czyszczenie i jak sprawdzić, czy doszło do włamania, ze wskaźnikami dla każdej fali.
- 2026-11-11: Przeniesiono z bloga do sekcji podatności. Dodano falę tensorlake z 8 października 2026 roku, datę powrotu z września 2026 roku, CVE-2026-45321 i jego wpis w KEV dla fali TanStack oraz package firewall GitHub.
- 2026-09-25: Pierwsza publikacja na blogu CRACI.