npm install nie powinno znaczyć: „uruchom obcy kod na moim komputerze”
Dlaczego skrypty instalacyjne zależności stanowią ryzyko i jak bezpiecznie korzystać z npm oraz pnpm.
npm install nie powinno znaczyć: „uruchom obcy kod na moim komputerze”
Wpisujesz w terminalu:
npm installalbo:
pnpm installSpodziewasz się pobrania bibliotek potrzebnych aplikacji. Przez wiele lat w ekosystemie Node.js ta operacja oznaczała jednak coś więcej: pobierz kod innych osób i automatycznie uruchom część tego kodu na moim komputerze.
To rozróżnienie ma znaczenie. Pobranie pliku i wykonanie programu to dwie różne decyzje dotyczące zaufania.
Od 2026 roku npm 12 domyślnie blokuje skrypty instalacyjne zależności, dopóki projekt jawnie ich nie zatwierdzi. To duża i potrzebna zmiana. Starsze wersje npm, inne narzędzia oraz późniejsze polecenia takie jak build czy test mogą jednak nadal uruchamiać kod zależności. Problem nie zniknął — stał się po prostu lepiej widoczny. Dokumentacja npm opisuje obecny mechanizm allowScripts, a GitHub wyjaśnia zmianę domyślnego zachowania w npm 12.
Co właściwie uruchamia się podczas instalacji?
Pakiet może zawierać skrypty cyklu życia, między innymi:
preinstall
install
postinstallLegalne pakiety używają ich na przykład do kompilowania dodatku napisanego w C++, pobierania pliku binarnego odpowiedniego dla systemu albo przygotowania zasobów.
Technicznie taki wpis:
{
"scripts": {
"postinstall": "node setup.js"
}
}nie jest jednak niewinną instrukcją konfiguracyjną. To polecenie uruchomienia programu setup.js.
Jeżeli program działa bezpośrednio na laptopie, otrzymuje uprawnienia zalogowanego użytkownika. Może próbować czytać pliki, uruchamiać kolejne procesy i łączyć się z internetem. Interesującymi celami są na przykład:
~/.npmrc tokeny do publikowania pakietów
~/.ssh/ klucze SSH
~/.aws/ dane dostępowe do AWS
~/.kube/config dostęp do klastrów Kubernetes
.git-credentials zapisane dane GitNa komputerze programisty może to oznaczać kradzież kodu i kont. Na serwerze CI szkody bywają większe, ponieważ runner może mieć klucze do rejestru pakietów, repozytoriów, chmury i środowiska produkcyjnego.
To nie jest podatność o jednym numerze CVE. To ryzykowna granica zaufania: obcy kod dostaje możliwość działania z uprawnieniami hosta.
Cztery incydenty, które pokazują eskalację problemu
1. ESLint, 2018: z pakietu do tokena publikacyjnego
W lipcu 2018 roku napastnik przejął konto jednego z maintainerów ESLint i opublikował złośliwe wersje:
eslint-scope@3.7.2
eslint-config-eslint@5.0.2Ich postinstall pobierał kod z Pastebin, wykonywał go i wysyłał napastnikowi zawartość lokalnego pliku .npmrc. W tym pliku często znajdował się token pozwalający publikować kolejne paczki.
Skutek miał więc dwa poziomy:
- kradzież sekretu z komputera ofiary;
- możliwość wykorzystania tego sekretu do zatrucia następnych pakietów.
npm unieważnił tokeny wydane przed incydentem. Nie ma publicznych dowodów, że skradzione tokeny posłużyły wtedy do masowego, dalszego rozprzestrzeniania ataku. Sam mechanizm został jednak zademonstrowany bardzo wyraźnie. Oficjalny postmortem ESLint.
Nietypowy szczegół: według postmortem konto maintainera nie miało włączonego 2FA, a jego hasło zostało wcześniej użyte również w innych serwisach. Supply-chain może więc zacząć się nie od błędu w kodzie, lecz od ponownie użytego hasła.
2. ua-parser-js, 2021: XMRig i DanaBot na komputerach developerów
W październiku 2021 roku przejęto konto autora popularnego ua-parser-js. Złośliwe wersje 0.7.29, 0.8.0 i 1.0.0 zawierały skrypt preinstall.js, który rozpoznawał system operacyjny i dobierał ładunek.
Atak instalował:
- XMRig — koparkę kryptowaluty Monero, zużywającą zasoby procesora ofiary;
- DanaBot na Windows — trojana bankowego i złodzieja danych uwierzytelniających.
To nie był „złośliwy fragment JavaScriptu działający w aplikacji”. Skrypt instalacyjny pobierał i uruchamiał program na poziomie systemu operacyjnego. Mandiant śledził ten incydent jako działalność klastra UNC3379. Analiza Mandiant opisuje oba ładunki.
GitHub zalecał traktowanie urządzenia, na którym zainstalowano złośliwą wersję, jako w pełni skompromitowanego: usunąć pakiet, ale także obrócić wszystkie klucze i sekrety z innego, czystego komputera. Samo npm uninstall nie usuwa programu, który paczka zdążyła już zainstalować. GitHub Advisory: CVE-2021-4229 / GHSA-pjwm-rvh2-c87w.
3. coa i rc, 2021: hasła, zrzuty ekranu i keylogger
Kilka tygodni później przejęto pakiety coa i rc. Złośliwy preinstall uruchamiał na Windows łańcuch złożony z compile.js, pliku wsadowego i pobranej biblioteki DLL ładowanej przez systemowy regsvr32.exe.
DLL:
- kradła hasła z przeglądarek;
- szukała danych logowania w klientach FTP, VNC i poczty;
- wykonywała zrzuty ekranu;
- rejestrowała naciśnięcia klawiszy.
To zestaw funkcji charakterystyczny dla credential stealera połączonego z keyloggerem. Mandiant powiązał infrastrukturę tych incydentów z DanaBotem, natomiast CERT-EU w swoim komunikacie ostrożnie opisuje funkcje DLL bez przesądzania nazwy rodziny. CERT-EU SA2021-062 zawiera wersje pakietów, pliki i inne wskaźniki kompromitacji.
4. Shai-Hulud, 2025–2026: malware, które publikowało własne kopie
Shai-Hulud zmienił skalę zagrożenia. Był robakiem supply-chain: nie tylko kradł tokeny, ale próbował używać ich do publikowania zainfekowanych wersji kolejnych pakietów należących do tej samej osoby.
Pierwsza fala z 2025 roku wykorzystywała głównie postinstall. Shai-Hulud wyszukiwał między innymi tokeny npm i GitHub oraz dane dostępowe do AWS, Azure i Google Cloud. Skradziony token publikacyjny zamieniał jedną ofiarę w kolejne źródło infekcji. GitHub opisuje tę samoreplikację i wielofalowy charakter kampanii w podsumowaniu zabezpieczeń npm.
W Shai-Hulud 2.0 z listopada 2025 roku łańcuch zaczynał się wcześniej, od preinstall. Skrypt setup_bun.js sprawdzał, czy dostępny jest runtime Bun, a w razie potrzeby go instalował. Następnie uruchamiał właściwy moduł wykradający dane.
Wariant ten potrafił również:
- publikować skradzione dane w repozytoriach GitHub;
- rejestrować urządzenie jako self-hosted runner GitHub Actions, tworząc kanał do późniejszego wykonywania poleceń;
- infekować kolejne pakiety za pomocą tokenów npm;
- w określonych warunkach uruchamiać destrukcyjne polecenia usuwające dane.
Datadog zidentyfikował co najmniej 796 pakietów i 1092 ich wersje zawierające Shai-Hulud 2.0. Liczby te opisują zidentyfikowane artefakty, a nie liczbę komputerów, na których malware na pewno się wykonało. Analiza Datadog Security Labs udostępnia też listę wskaźników kompromitacji.
W maju 2026 roku Microsoft opisał kolejny wariant, Mini Shai-Hulud: ponad 170 pakietów npm, dwa pakiety PyPI i łącznie 404 złośliwe wersje. Ciekawostką był mechanizm maskowania błędu: złośliwa zależność opcjonalna wykonywała payload, po czym celowo kończyła pracę błędem. npm mógł potraktować to jak nieudaną instalację opcjonalnego dodatku i kontynuować, podczas gdy kod już się wykonał. Malware dodawało też mechanizm trwałości do konfiguracji Claude Code. Analiza Microsoft Security.
Czego te incydenty dowodzą — a czego nie
Dowodzą, że skrypt zależności uruchomiony na hoście może wykorzystać wszystko, do czego ma dostęp użytkownik lub runner CI.
Nie dowodzą, że każdy skrypt instalacyjny jest złośliwy. Nie dowodzą też, że wyłączenie postinstall rozwiązuje problem. Kod zależności uruchamiają również bundlery, generatory kodu, test runnery i polecenia takie jak:
npm run buildDlatego właściwe pytanie nie brzmi: „Czy ten projekt ma postinstall?”. Brzmi: jaki obcy kod uruchamiamy i do czego będzie miał dostęp?
Git pokazuje lepszą wartość domyślną: clone != execute
Git także ma hooki, czyli programy uruchamiane przy zdarzeniach takich jak commit, checkout czy push. Tradycyjnie znajdują się one w lokalnym .git/hooks/.
Kluczowa różnica polega na tym, że aktywne hooki i lokalna konfiguracja nie są zwykłą, wersjonowaną zawartością kopiowaną przez git clone. Dokumentacja Git w sekcji SECURITY mówi wprost: konfiguracja i hooki mogą wykonywać dowolne polecenia, lecz nie są kopiowane przy klonowaniu, dlatego samo sklonowanie i przeglądanie niezaufanej zawartości jest co do zasady bezpieczne.
„Co do zasady” nie oznacza „zawsze”. Nie należy uruchamiać poleceń w katalogu .git otrzymanym bezpośrednio od niezaufanej osoby, a błędy implementacyjne mogą tę granicę przełamać.
Już w 2006 roku Linus Torvalds, odpowiadając w dyskusji zatytułowanej „Security problem”, rozróżniał bazę obiektów pobieraną z repozytorium od lokalnego stanu checkoutu i indeksu. Nie jest to dowód, że przewidział współczesne ataki na package managery. Jest to jednak wczesny przykład świadomego oddzielania transportowanych danych od lokalnego stanu wykonawczego. Archiwum dyskusji na liście Git.
CVE-2024-32002: path traversal zamienił clone w RCE
W 2024 roku odkryto krytyczną podatność CVE-2024-32002, sklasyfikowaną między innymi jako CWE-22: path traversal.
Specjalnie przygotowane repozytorium z submodułami mogło na systemie plików niewrażliwym na wielkość liter i obsługującym symlinki oszukać Git. Zamiast zapisać plik w katalogu roboczym submodułu, Git umieszczał wykonywalny hook wewnątrz .git/. Hook uruchamiał się jeszcze w trakcie:
git clone --recurse-submodules ...Skutkiem było RCE — remote code execution, czyli wykonanie na komputerze ofiary poleceń wybranych przez autora złośliwego repozytorium, zanim użytkownik zdążył obejrzeć kod.
Oficjalny advisory ocenił podatność na 9,0/10 i wymienia poprawione wydania, między innymi Git 2.45.1, 2.44.1 i 2.43.4. GHSA-8h77-4q3w-gfgv / CVE-2024-32002.
Ta podatność była groźna właśnie dlatego, że omijała normalną zasadę Gita: zdalne repozytorium nie powinno instalować lokalnych hooków.
CVE-2026-52726 w Dulwich: ta sama granica, inny błąd
W 2026 roku podobny problem znaleziono w Dulwich, napisanej w Pythonie implementacji formatów i protokołów Git.
CVE-2026-52726 to również path traversal (CWE-22). Funkcje submodule_update() oraz clone(..., recurse_submodules=True) niewłaściwie sprawdzały ścieżkę submodułu. Złośliwe .gitmodules mogło wskazać jako cel dosłownie:
.git/hooksDulwich zapisywał tam pliki napastnika i zachowywał ich wykonywalne bity. Późniejsza operacja Git lub Dulwich wywołująca odpowiedni hook prowadziła do RCE.
W odróżnieniu od CVE-2024-32002 ten atak nie wymagał systemu plików niewrażliwego na wielkość liter: literalna ścieżka .git/hooks działała na Linuxie, macOS i Windows. Podatne były wersje od 0.23.2 do 1.2.4; poprawkę wydano w Dulwich 1.2.5. GHSA-gfhv-vqv2-4544 / CVE-2026-52726.
Oba CVE pokazują tę samą regułę: napastnik często nie potrzebuje nowego mechanizmu wykonania. Wystarczy, że zapisze swój plik w miejscu, które system już uważa za uprawnione do uruchamiania kodu.
Skoro kod trzeba wykonać, ograniczmy mu przestrzeń
Budowanie aplikacji wymaga uruchamiania kodu. Nie da się zredukować ryzyka do hasła „wyłącz wszystkie skrypty”. Można natomiast zmniejszyć zakres szkód.
W GoatCMS zadanie instalacji i buildu może zostać uruchomione w odseparowanym kontenerze, na przykład:
run:docker --image=node:22 --interactive=false --body=<<WEBEOF
set -e
mkdir -p /tmp/bin
corepack enable --install-directory /tmp/bin
export PATH="/tmp/bin:$PATH"
pnpm install --frozen-lockfile
pnpm build
WEBEOFPolecenia pozostają te same. Zmienia się otoczenie procesu. Zależność nie musi widzieć katalogu domowego hosta, jego kluczy SSH ani konfiguracji chmury. Dostaje jedynie zasoby jawnie przekazane do zadania.
To jest opis modelu bezpieczeństwa GoatCMS, a nie twierdzenie, że każdy kontener automatycznie zapewnia pełną izolację.
Kontener może mieć tylne drzwi wielkości bramy
Takie uruchomienie nadal będzie ryzykowne:
docker run \
--privileged \
-v /:/host \
-v /var/run/docker.sock:/var/run/docker.sock \
...--privileged rozszerza możliwości procesu. Montaż / udostępnia system plików hosta. Socket Dockera pozwala wydawać polecenia demonowi, który może tworzyć kolejne kontenery i montować pliki hosta. W praktyce jest to często ścieżka do bardzo szerokiej kontroli nad maszyną. Dokumentacja bezpieczeństwa Docker Engine opisuje znaczenie ochrony dostępu do demona.
Kontener ogranicza szkody tylko wtedy, gdy rzeczywiście ograniczamy jego możliwości:
- montujemy wyłącznie potrzebne katalogi;
- przekazujemy tylko niezbędne, krótkotrwałe sekrety;
- nie używamy
--privilegedbez konieczności; - nie montujemy socketa Dockera;
- ograniczamy ruch wychodzący, jeśli build nie potrzebuje internetu;
- po wykonaniu zadania usuwamy środowisko;
- przypinamy obraz do konkretnego digestu, gdy potrzebujemy powtarzalności.
Lockfile pomaga, ale nie wystarcza
Lockfile zapisuje dokładne wersje zależności. Dzięki temu nowa, złośliwa wersja nie powinna zostać pobrana tylko dlatego, że mieści się w szerokim zakresie wersji z package.json.
Nie jest to jednak certyfikat bezpieczeństwa. Jeśli złośliwa wersja została już zaakceptowana i zapisana w lockfile, kolejne instalacje będą ją odtwarzać bardzo konsekwentnie.
Warto połączyć kilka warstw:
- przypięte zależności i obowiązkowy review zmian lockfile;
- jawne zatwierdzanie skryptów instalacyjnych;
- MFA odporne na phishing i publikowanie przez OIDC zamiast długowiecznych tokenów;
- izolowane, jednorazowe środowiska buildów;
- minimalne mounty, uprawnienia i sekrety;
- ograniczenia sieciowe i monitoring prób eksfiltracji;
- sprawdzanie pochodzenia artefaktu, nie tylko linku do repozytorium.
Ciekawy sygnał zmiany w branży: npm 12 blokuje skrypty zależności bez zatwierdzenia. Wcześniej nawet --ignore-scripts nie zamykało wszystkich dróg. Zależność instalowana z Gita mogła zawierać .npmrc, który podmieniał program używany jako Git i osiągał wykonanie kodu inną ścieżką. Dlatego w npm 11 pojawiło się --allow-git, a npm 12 domyślnie ograniczył również zależności gitowe. Opis mechanizmu --allow-git.
Nie pytaj tylko, czy ufasz autorowi
Autor pakietu nie musi mieć złych intencji. Wystarczy przejęte konto, wykradziony token, podatny proces publikacji albo skompromitowana zależność przechodnia.
Pytanie „czy ten maintainer jest godny zaufania?” jest więc za wąskie. Lepsze brzmi:
Co ten kod będzie mógł zrobić, jeśli konto autora zostanie przejęte jutro?
Git ujmuje dobrą wartość domyślną w prostej zasadzie:
clone != executeSystemy buildowe potrzebują podobnych granic:
download != trust
install != dostęp do hosta
build != nieograniczone uprawnieniaIzolacja nie sprawi, że złośliwy kod stanie się bezpieczny. Może jednak sprawić, że jedna przejęta paczka nie przejmie przy okazji laptopa developera, kluczy do chmury i całego procesu publikacji.
To jest właściwy cel konteneryzowanych skryptów w GoatCMS: nie obietnica, że każda zależność jest uczciwa, tylko założenie, że któraś kiedyś nie będzie — i przygotowanie systemu na ten dzień.
Źródła
npm i incydenty supply-chain
- npm: lifecycle scripts
- npm 12: install scripts jako opt-in
- ESLint: postmortem złośliwych publikacji z 2018 roku
ua-parser-js: CVE-2021-4229 / GHSA-pjwm-rvh2-c87w- Mandiant:
ua-parser-js,coairc - CERT-EU: przejęcie
coairc - GitHub: wielofalowa kampania Shai-Hulud
- Microsoft: Shai-Hulud 2.0 i Mini Shai-Hulud
- Datadog: skala i wskaźniki Shai-Hulud 2.0