Strona główna / Blog / npm: malware, nie CVE

Blog · case study

150 tys. downloadów tygodniowo, 10 złośliwych wersji w 21 minut: kiedy SCA musi przestać pytać o CVE i zacząć pytać „czy ten build już wykonał malware?”

W klasycznym Software Composition Analysis świat wygląda prosto: dependency → known vulnerability → CVE → affected version → fixed version → upgrade. To użyteczny model. Ale nie działa wystarczająco dobrze wtedy, gdy package nie jest po prostu „podatny” — jest złośliwy.

28 sierpnia 2026 atakujący opublikował 10 złośliwych wersji popularnego pakietu npm @7nohe/openapi-react-query-codegen, używanego do generowania type-safe hooków TanStack Query z definicji OpenAPI. Według Aikido pakiet miał ponad 150 000 pobrań tygodniowo. Dziesięć złośliwych wydań pojawiło się w dwóch falach między ~20:00:43 a 20:20:53 UTC — w około 21 minut. Najważniejsze pytanie dla VM nie brzmi jednak „czy mamy złą wersję dependency?”, tylko: czy ta wersja została tylko pobrana, czy jej kod już wykonał się na workstation albo runnerze CI? Od tego momentu nie mówimy o remediation dependency — mówimy o potencjalnym incydencie.

Dziesięć złośliwych wersji

0.5.4 0.5.5 1.6.3 1.6.4 2.2.1 2.2.2 3.0.3 3.0.4 0.0.0-365d4eb738d3146583431948d3ba6e27a32556be 0.0.0-ec7876d6c917dad516ba69bbfafc948b834bf0ab

Pierwsza fala ~20:00–20:02 UTC, druga ~20:19–20:21 UTC. W części tego okna tag latest wskazywał na złośliwe 3.0.4. To ważne: do infekcji nie trzeba było świadomie wybierać egzotycznej prerelease — zwykły mechanizm instalacji lub aktualizacji mógł pobrać wersję, którą registry uznawało w danym momencie za najnowszą.

To nie był klasyczny „skradziony npm token”

Najciekawszym elementem jest sposób publikacji. Według StepSecurity, Mend.io i Socket atakujący nie musiał znać hasła maintenera ani kraść długoterminowego tokenu npm. Problem siedział w GitHub Actions: workflow reagował na issue_comment i uruchamiał publishing po komentarzu o odpowiedniej treści, ale brakowało sprawdzenia, kim jest autor komentarza. Następnie checkoutował kod z pull requestu, działał z id-token: write i korzystał z npm Trusted Publishing/OIDC:

open PR from fork → post trigger comment → privileged release workflow executes → OIDC token minted → malicious package published

Lekcja dla DevSecOps: Trusted Publishing usuwa problem długowiecznych sekretów, ale nie usuwa problemu kto może uruchomić workflow posiadający prawo publikacji.

Prawidłowa provenance nie oznacza bezpiecznego artefaktu

To szczególnie niewygodne. Złośliwe wydania mogły mieć prawidłową provenance z GitHub Actions — bo rzeczywiście opublikował je prawidłowy workflow. Tyle że workflow został wykorzystany jako narzędzie atakującego: authentic provenance != benign content. Provenance odpowiada „skąd pochodzi artifact?”, nie zawsze „czy artifact jest bezpieczny?”. Jeżeli atakujący przejmie zaufany proces build/release, może produkować autentycznie podpisane malware.

binding.gyp: malware bez oczywistego install script

Aikido zwróciło uwagę na mechanizm execution: część złośliwych wersji używała binding.gyp — pliku normalnie używanego przez node-gyp przy budowaniu native addons. Kod mógł dzięki temu uruchamiać się podczas npm install, nawet jeśli ktoś sprawdzał tylko preinstall/postinstall w package.json i nie widział niczego podejrzanego. Późniejsza fala dodała jawny preinstall, zwiększając niezawodność uruchomienia. Wniosek: „No install scripts” nie jest dowodem, że instalacja package nie wykona kodu.

Present nie znaczy executed

Standardowe SCA odpowie „repo A używa 3.0.4” albo „lockfile zawiera złośliwą wersję”. Ale dla malware potrzebujemy osi czasu: was it installed? when? where? under which identity? with which environment variables? To zupełnie inny problem — a stanów jest kilka:

Stan 1: wpis w package.json, wersja nigdy nie zainstalowana.
Stan 2: wpis w lockfile, build nieuruchomiony.
Stan 3: package w cache, execution niepotwierdzone.
Stan 4: npm install/npm ci wykonane — payload mógł się uruchomić.
Stan 5: złośliwy build w CI — runner miał GitHub token, npm/cloud credentials, SSH keys, deployment secrets.

Dla VM te stany nie powinny mieć jednego statusu Affected.

Malware finding potrzebuje innego lifecycle

Dla CVE często wystarcza OPEN → PATCHED → VERIFIED → CLOSED. Dla malicious package potrzeba czegoś bliższego incident response:

DETECTED → EXPOSURE_CONFIRMED → EXECUTION_UNKNOWN → EXECUTION_CONFIRMED → CONTAINED → CREDENTIALS_ROTATED → HOSTS_REBUILT/VALIDATED → CLOSED

Bo samo przejście na dobrą wersję nie wystarcza. Jeśli 3.0.4 → npm install → payload executes → token stolen, a potem developer instaluje czystą wersję, SCA powie „no malicious package present” — ale atakujący nadal może mieć skradziony token. Usunięcie package usuwa źródło kolejnych wykonań; nie cofa kompromitacji. To dokładnie różnica patch vulnerability vs remediate compromise.

Co mógł kraść payload — i dlaczego to worm

Analizy Aikido, StepSecurity, Socket i innych wskazują na próby dostępu do credentiali developerskich i CI/CD: GitHub, npm, PyPI, RubyGems, JFrog, AWS, Azure, Google Cloud, Vault. Pojedynczy infected build może być początkiem kolejnego supply-chain compromise. Jeśli malware ma mechanizm propagacji do innych pakietów utrzymywanych przez ofiarę: developer installs malicious dependency → credentials stolen → attacker gets publishing capability → additional packages poisoned → new victims → repeat. Wtedy jeden dependency finding nie opisuje już pojedynczego repo — opisuje potencjalny węzeł propagacji.

Co powinien zrobić Vulnerability Engineer?

  1. Znaleźć wszystkie wystąpienia wersji — nie tylko package.json, ale lockfiles (npm/pnpm/Yarn), build logs, npm caches, artifact metadata.
  2. Ustalić execution — czy w oknie publikacji wykonywano npm install/ci, pnpm install, yarn.
  3. Zidentyfikować maszyny — developer workstation, CI runner, ephemeral runner, build container.
  4. Ustalić dostępne sekrety w procesie i środowisku w czasie instalacji.
  5. Rotować (przy potwierdzonym lub prawdopodobnym execution): GitHub/registry tokens, cloud credentials, SSH keys.
  6. Sprawdzić downstream — czy konto maintenera publikowało nowe wersje innych pakietów po incydencie.

SCA musi więc przechowywać execution evidence (lockfile_present, installed, install_command_seen, host, secrets_at_risk, credentials_rotated) i status w rodzaju COMPROMISE_REMEDIATION_REQUIRED — to znacznie bardziej adekwatne niż „Critical dependency issue”. SBOM powie „mieliśmy package X w wersji Y”, ale nie „czy package wykonał się podczas build” — do tego trzeba CI/endpoint telemetry, build provenance i package-manager logs.

„No CVE” nie może blokować procesu

Malicious package może nigdy nie dostać CVE, bo problemem nie jest vulnerability w package, tylko to, że package celowo wykonuje złośliwe działania. Dlatego VM/SCA powinno mieć osobną klasę MALWARE obok VULNERABILITY, LICENSE, EOL, MISCONFIGURATION. Jeżeli organizacja reaguje tylko na CVE ID, przegapi jedną z najważniejszych klas supply-chain risk.

Wniosek

Incydent @7nohe/openapi-react-query-codegen pokazuje, że nowoczesne SCA musi wyjść poza package → CVE → upgrade i obsłużyć: package → malware → installation → code execution → credential theft → supply-chain propagation → incident response. Dziesięć złośliwych wersji przez ~21 minut przy pakiecie pobieranym ponad 150 tys. razy tygodniowo to nie tylko problem dependency management — to przykład, w którym build sam staje się punktem kompromitacji. I od tego momentu pytanie „czy już usunęliśmy package?” jest zdecydowanie za małe. Dla vulnerable dependency pytamy „czy kod może zostać wykorzystany?”; dla malicious dependency — czy kod już się wykonał?

Powiązane na blogu

Źródła

  • Aikido Security — Popular code generator for TanStack Query hit by supply chain worm (28.08.2026) — aikido.dev
  • StepSecurity — @7nohe/openapi-react-query-codegen compromised through an exposed npm publishing workflow (28.08.2026) — stepsecurity.io
  • Socket — OpenAPI React Query Codegen compromised in Mini Shai-Hulud npm supply chain attack (28.08.2026) — socket.dev
  • Mend.io — Mini Shai-Hulud hits openapi-react-query-codegen (29.08.2026) — mend.io

Nota redakcyjna: stan informacji 30 sierpnia 2026, 13:34 CEST. Potwierdzono 10 złośliwych wersji opublikowanych między ~20:00:43 a 20:20:53 UTC 28 sierpnia 2026. Aikido podaje ponad 150 tys. pobrań tygodniowo. Atrybucja do konkretnej grupy nie jest w tym artykule traktowana jako potwierdzona; istotny jest techniczny mechanizm kompromitacji i propagacji.

Zapamiętaj jedno

Dla vulnerable dependency pytasz „czy kod może zostać wykorzystany?”. Dla malicious dependency — „czy kod już się wykonał?”. present ≠ executed, a czysta wersja zainstalowana nie zamyka malware findingu.