Shai-Hulud-Malware-Angriff: Tinycolor und über 40 NPM-Pakete infiziert
(stepsecurity.io)- Im NPM-Ökosystem kam es zu einem Supply-Chain-Angriff, bei dem in mehr als 40 Pakete, darunter das beliebte @ctrl/tinycolor, selbstverbreitende Malware eingeschleust wurde. Dadurch können Geheimnisse aus Entwicklungsumgebungen bis hin zu CI/CD-Zugangsdaten kaskadierend infiziert werden. Die infizierten Versionen wurden aus npm entfernt.
- Die Angriffs-Payload führt während des npm-Installationsprozesses asynchron ein Webpack-Bundle (
bundle.js, ~3,6 MB) aus und sammelt über Umgebungsvariablen, Dateisystem und Cloud-SDKs umfangreich Credentials. - Die bösartige Logik erzwingt über NpmModule.updatePackage das Patchen und Veröffentlichen anderer Pakete und sorgt so für eine kaskadierende Verbreitung. Zudem injiziert sie in GitHub Actions den Workflow shai-hulud und exfiltriert Organisations-Secret über toJSON(secrets).
- Die gesammelten Daten werden durch das Anlegen des öffentlichen GitHub-Repositorys
Shai-Huludexfiltriert, getarnt als normale Entwicklungsaktivität und mit hoher Fähigkeit zur Umgehung von Erkennung. - Dies geschieht verdeckt über Zugriffe auf AWS/GCP/Azure/NPM/GitHub-Token und Metadata-Endpunkte sowie über Secret-Suche auf Basis von TruffleHog.
- Erforderlich sind die sofortige Entfernung der Pakete, die Bereinigung der Repositories und der Austausch sämtlicher Credentials sowie die Prüfung von CloudTrail-/GCP-Audit-Logs, das Blockieren von Webhooks und die Einführung von Branch Protection / Secret Scanning / Cooldown-Richtlinien.
Affected Packages
- Berichtet wurden insgesamt 195 Pakete/Versionen. Dazu zählen unter anderem @ctrl/tinycolor (4.1.1, 4.1.2), zahlreiche Pakete im Namespace @ctrl/, die Modulgruppe @crowdstrike/, ngx-bootstrap/ngx-toastr/ng2-file-upload/ngx-color und damit große Teile des Angular-/Web-UI-Ökosystems, außerdem der Mobile-Stack @nativescript-community/ und @nstudio/, die Life-Sciences-Toolchain teselagen/, ember-*, koa2-swagger-ui, pm2-gelf-json und wdio-web-reporter.
- Die exakten Versionen je Paket sind der Tabelle im Original zu entnehmen; es ist eine präzise Querverifikation erforderlich, ob diese Versionen verwendet werden.
- Beispiele:
@ctrl/ngx-emoji-mart 9.2.1, 9.2.2,@ctrl/qbittorrent 9.7.1, 9.7.2,ngx-bootstrap 18.1.4, 19.0.3–20.0.5,ng2-file-upload 7.0.2–9.0.1usw.
- Beispiele:
Immediate Actions Required
Identify and Remove Compromised Packages
- Im Projekt prüfen, ob infizierte Pakete vorhanden sind: z. B. mit
npm ls @ctrl/tinycolor - Infizierte Pakete sofort entfernen: z. B. mit
npm uninstall @ctrl/tinycolor - Lokale Spuren durch Suche nach dem bekannten
bundle.js-Hash prüfen:sha256sum | grep 46faab8a...verwenden
Clean Infected Repositories
- Bösartigen GitHub-Actions-Workflow löschen:
.github/workflows/shai-hulud-workflow.ymlentfernen - Auf dem Remote erstellten Branch
shai-huluderkennen und löschen: nachgit ls-remote ... | grep shai-huluddanngit push origin --delete shai-huludausführen
Rotate All Credentials Immediately
- NPM-Token, GitHub-PATs/Actions-Secrets, SSH-Schlüssel, AWS/GCP/Azure-Zugangsdaten, DB-Connection-Strings, Third-Party-Token, CI/CD-Secrets usw. müssen vollständig ersetzt werden
- Erforderlich ist eine vollständige Rotation, einschließlich aller Einträge in AWS Secrets Manager/GCP Secret Manager
Audit Cloud Infrastructure for Compromise
- AWS: In CloudTrail Zeitpunkte und Muster von Aufrufen wie
BatchGetSecretValue,ListSecrets,GetSecretValueprüfen; mit dem IAM Credential Report auf anomale Erstellung oder Nutzung von Schlüsseln untersuchen - GCP: In den Audit Logs die Zugriffe auf Secret Manager prüfen und kontrollieren, ob CreateServiceAccountKey-Ereignisse vorhanden sind
1 Kommentare
Hacker-News-Kommentare
Aus der Perspektive von jemandem, der Pakete nutzt, die auf npm gehostet werden, ist es kein realistischer Ansatz, alle Abhängigkeiten und sogar die Abhängigkeiten dieser Abhängigkeiten direkt zu überwachen. Ich bin auch kein TypeScript-/JavaScript-Experte, daher würde ich vermutlich nicht leicht versteckten Schadcode entdecken, den ein Angreifer eingebaut hat. Worüber ich in letzter Zeit nachdenke, ist eine Methode zum Aktualisieren im „verzögerten Modus“, also nur auf Versionen zu aktualisieren, die nicht die neueste sind, sondern schon eine gewisse Zeit verfügbar waren. Die Idee ist, dass bei einem Paket, das etwa 6 Wochen öffentlich verfügbar war, die Wahrscheinlichkeit hoch ist, dass Schadcode bereits aufgedeckt worden wäre. Perfekt ist das aber nicht, und es wäre schön, wenn es ein Tool gäbe, das in Sicherheitsfällen ausnahmsweise dennoch sofort das neueste Update anwenden kann.
Die im Artikel direkt erwähnte Methode ist die Funktion NPM Package Cooldown Check. Wenn eine Paketversion, die innerhalb des von der Organisation festgelegten Zeitraums (standardmäßig 2 Tage) veröffentlicht wurde, zu einem Pull Request hinzugefügt wird, schlägt der Build automatisch fehl. Da die meisten Supply-Chain-Angriffe innerhalb von 24 Stunden entdeckt werden, kann schon eine sehr kurze Wartezeit das Sicherheitsrisiko verringern.
Da es schwierig ist, wirklich alle Abhängigkeiten zu prüfen, würde ich dafür plädieren, die Zahl der Abhängigkeiten so weit wie möglich zu reduzieren und nur bekannte, vertrauenswürdige Pakete zu verwenden. Wenn man nicht in einer so kontrollierten Umgebung arbeitet, dass man tatsächlich allen Autoren vertrauen kann, ist es eher eine vernünftige Entscheidung, einen gewissen Grad an „not-invented-here“ beizubehalten.
Ich habe mir angewöhnt, Versionen in der
package.jsonexplizit festzupinnen und mitnpm cinur die inpackage-lock.jsonfestgelegten Versionen zu installieren. In CI lasse ichnpm auditlaufen und bekomme einen Alarm, wenn in Paketen Schwachstellen auftauchen. Auf diese Weise sind die Pakete fast in einem „eingefrorenen“ Zustand, und allein das Alter der Pakete senkt die Wahrscheinlichkeit einer Infektion.Bei mir geht das noch weiter: Ich aktualisiere Abhängigkeiten nur dann, wenn ein Bug meine tatsächliche Einsatzumgebung beeinflusst. Selbst Sicherheitslücken ignoriere ich, wenn sie mich nicht betreffen. Die meisten Entwickler aktualisieren Abhängigkeiten unnötig oft, aber ich denke, man sollte das nur tun, wenn es wirklich nötig ist. Wenn Updates häufig nötig oder kompliziert sind, verwende ich das Paket entweder gar nicht erst oder „friere“ es nach meinem Maßstab ein.
Mit
uvfür Python kann man Updates in ähnlicher Form einschränken. Mit einem Befehl wieuv lock --exclude-newer $(date --iso -d "2 days ago")lassen sich beispielsweise Versionen ausschließen, die innerhalb der letzten 2 Tage veröffentlicht wurden.Solche Probleme entstehen, weil neue Pakete oder Versionen nicht überwacht werden. Am besten wäre es, wie bei Debian eine stabile Distribution zu betreiben, in die ausschließlich Sicherheits-Patches und Bugfixes einfließen, und davon getrennt eine testing-/unstable-Distribution, die von Paket-Maintainern überwacht wird. Alle, die mit zentralisierten Open-Package-Repositories arbeiten (NPM, Python, Rust usw.), haben dasselbe Problem.
Es gibt ein Problem in der Entwicklerkultur. Hunderte von (transitiven) Abhängigkeiten zu haben und sie gedankenlos automatisch zu aktualisieren, ist an sich schon das Problem. Wer so viele Drittanbieter-Codebasen der Build-/Runtime-Umgebung aussetzt, trägt dafür Verantwortung.
Auch Distributionen spüren zunehmend die Last der Menge an Paketen, die sie pflegen müssen. Genau deshalb haben sich eigentlich sprachspezifische Ökosysteme entwickelt, etwa CPAN, Maven oder RubyGems. Linux-Distributionen allein konnten die Anwendungen, die Nutzer wollten, nur schwer bereitstellen, weshalb Wege wie freshmeat, linuxbrew, flatpak oder PPA entstanden sind. Ich glaube nicht, dass jede Community die Kapazität hat, unzählige verschiedene Bibliotheken in mehreren Branches zu überwachen und zu unterstützen.
Als Debian-Entwickler wird es wegen immer mehr „Rauschen“ vor der Übernahme von Upstream-Code zunehmend schwieriger, reale Änderungen zu erkennen, insbesondere durch reine Stiländerungen oder Tooling-Updates. Solche Änderungen würde ich gern zurückgedrängt sehen, sofern es sich nicht um Refactorings, Bugfixes, neue Features oder Tooling-Ergebnisse handelt, die tatsächlich menschliche Prüfung erfordern oder problematischen Code aufspüren sollen.
In Rust gibt es ein System namens cargo vet. Daran beteiligen sich Unternehmen wie Google und Mozilla, die Pakete gemeinsam automatisch prüfen und validieren.
Ich denke, es gibt Wege, eine gewisse Absicherung zu schaffen und trotzdem dezentral zu bleiben. Man könnte etwa verlangen, dass Pakete ab einer bestimmten Größe von zwei Accounts mit 2FA freigegeben werden müssen, oder dass populäre Pakete nur noch über reproduzierbare Build-Systeme auf npm hochgeladen werden dürfen. Damit würde man vollständige Dezentralität nicht aufgeben, sondern nur bei größeren Projekten etwas zusätzlichen Aufwand verlangen.
Wegen der anhaltenden Supply-Chain-Angriffe in letzter Zeit denke ich ernsthafter über Server-Rendering ohne JavaScript nach. Dank HTMX ist mir klar geworden, wie weit man auch ohne JavaScript kommen kann. Außerdem scheint die Anwendung auf diese Weise sogar schneller und stabiler zu werden.
Ich möchte betonen, dass die traditionelle JS-Umgebung in Wirklichkeit die sicherste Sandbox-Umgebung ist. Seit fast 30 Jahren läuft nicht vertrauenswürdiger JS-Code auf Milliarden von Geräten, aber erfolgreiche groß angelegte Angriffe auf Browser-Engines lassen sich an einer Hand abzählen. Die NodeJS- und npm-Umgebung dagegen braucht sicherheitstechnisch eine komplette Neugestaltung. Dinge wie leftpad stammen aus einer Kultur, in der selbst einfache Code-Snippets auf npm hochgeladen werden.
Es wirkt merkwürdig, dass solche Angriffe automatisch auf ein bestimmtes Umfeld, nämlich JavaScript, verengt diskutiert werden. Das größere Problem scheint mir zu sein, dass selbst die Sicherheitsmaßnahmen, die es immerhin für npm gibt, in anderen Umgebungen wie PyPI oder Crates überhaupt nicht angewendet werden.
Vendoring kann das Risiko verringern, ist aber meiner Einschätzung nach keine grundlegende Lösung. Wenn NPM Sicherheit ernst nähme, müssten 2FA und ein vorgeschalteter Paket-Scan für Veröffentlichungen verpflichtend sein, einschließlich einer erzwungenen Signatur mit Hardware-Schlüsseln. Semver oder CRC reichen dafür nicht aus. Das alles müsste standardmäßig im Paketmanagement-System eingebaut sein.
Eigentlich ist das kein Problem nur von JavaScript. Es entsteht dadurch, dass Entwickler beim Hinzufügen neuer Abhängigkeiten nicht ausreichend hinschauen. Das kann genauso gut in anderen Sprachökosystemen wie Rust oder Go passieren.
Alle Sprachen, die stark von Paketmanagern abhängen und nur eine magere Standardbibliothek haben, sind gleichermaßen anfällig. Langfristig denke ich, dass wir wieder mehr in Richtung Vanilla JavaScript gehen sollten. Rust hat in gleicher Weise eine hohe Paketabhängigkeit. Go ist in dieser Hinsicht eher ein Beispiel dafür, wie man es besser machen kann.
Ich denke, wir brauchen ein System, das Commits und Releases mit vertrauenswürdigen Schlüsseln signiert und nachvollziehbaren, schlanken Code für Installation und Verifikation einsetzt. Es gibt bereits npm-Provenance mit sigstore, aber das scheint bisher weder breit genutzt zu werden noch über die Prüfung des Herausgebers hinauszugehen.
Schon 2016 wurde diese Schwachstelle in NPM gemeldet (CERT-Hinweis), aber die Antwort von NPM lautete WAI (working as intended).
Für alle, die nicht wissen, was WAI bedeutet: In der Regel steht es für „working as intended“.
Selbst wenn es gar kein
postinstall-Skript gäbe, würde der Schadcode wohl spätestens beim Importieren des Moduls im Build-Prozess, beim Serverstart oder in Tests ausgeführt werden. Irgendwann nachnpm installläuft am Ende immer wirklich etwas ...Ich muss an einen Kommentar denken, den ich hier während der left-pad-Affäre gesehen habe. Da hieß es, ein bekannter npm-Maintainer habe 600 npm-Pakete und 1.200 Zeilen JavaScript-Code. Ein Beispiel, das ich gern hervorheben würde, ist esbuild: fast keine externen Abhängigkeiten, stattdessen nur die Go-Standardbibliothek.
Auch andere Projekte, die als „next generation“ gelten, haben in ihrer Abhängigkeitskette ziemlich wenig, etwa biomejs und swc. Wenn man sich aber den ursprünglichen Rust-Code ansieht, haben auch biomejs und swc letztlich viele Abhängigkeiten. Wenn sich solche Projekte weiter verbreiten, wird das cargo-Ökosystem wohl denselben Weg gehen. Falls jemand große Projekte kennt, die ähnlich streng wie esbuild geschrieben sind, würde ich mich über Empfehlungen freuen.
Einer der Gründe, warum ich zu Go gewechselt bin, ist der Trend zu Bibliotheken im purego-Stil. Sie hängen meist nur von der Standardbibliothek und
golang.org/xab, lassen sich ohne CGO kompilieren und sind dadurch sehr portabel. Mitgo mod vendorkann man kurzfristig Risiken managen, aber das ist keine grundlegende Lösung. Auch Go bietet keine Ende-zu-Ende-Paketvalidierung mit Signaturen oder Schlüsselprüfungen, weshalb letztlich Schwachstellen bleiben. Vieles konzentriert sich besonders auf CI/CD-Infrastruktur, aber wenn Builds und Deployments ohne Weitergabe von Signaturschlüsseln möglich wären, könnte das die Sicherheit erhöhen. Paketmanager sollten GPG-Signaturen fördern, und auch Git-Commits sollten signiert ausgeliefert werden.Besonders frustrierend finde ich den Fall von eslint. Wenn man sich den Dependency Graph ansieht, ist er enorm, und wenn Maintainer die Verringerung von Abhängigkeiten nicht priorisieren, bleibt am Ende nur der Wechsel zu einer anderen Lösung wie oxlint.
Die Lösung besteht darin, einfache Funktionen selbst zu bauen und externe Abhängigkeiten zu reduzieren. Oft lässt sich allein damit schon rund zwei Drittel der gesamten Abhängigkeiten einsparen. Gerade Dinge wie left-pad kann man selbst schreiben und mit kleinen Units und Tests unter eigener Kontrolle halten, ohne dass der Wartungsaufwand besonders hoch wäre. Unnötige Abhängigkeiten sollte man konsequent ausschließen.
Was im Root-
Cargo.tomleines Rust-Projekts steht, gilt für den gesamten Workspace; die tatsächlichen Abhängigkeiten der einzelnen Crates sind deutlich flacher. Man muss tiefer schauen, um die wirkliche Abhängigkeitsstruktur zu verstehen.Der Nachteil ist, dass man zum Prüfen von JavaScript-Projekten jetzt auch noch Golang lesen können muss. Und dann wird bei
post-installwiedernode install.jsausgeführt, sodass am Ende nichts anderes bleibt, als entweder vollständig zu vertrauen oder den Code komplett zu lesen.Ich kann nicht glauben, dass npm standardmäßig immer noch die
postinstall-Skripte aller Abhängigkeiten ausführt. Pnpm oder Bun führen sie nur aus, wenn sie auf einer Allowlist stehen, und Composer führt Lifecycle-Skripte bei Abhängigkeiten überhaupt nicht aus. Wegen des Risikos, das abhängige Pakete für Build- oder Entwicklungsumgebungen darstellen, scheint mir dieser Ansatz sicherer.Ich frage mich, warum man von solchen groß angelegten Angriffen bei anderen Paketmanagern, etwa Rust
build.rs, Python oder Java, nicht so oft hört. Theoretisch ist so etwas, nicht nurpostinstall, sondern im Prinzip in fast allen Ökosystemen möglich, und doch scheinen sich die Vorfälle vor allem auf npm zu konzentrieren.Ich habe gesehen, dass sich der Standardwert von Pnpm auf das Blockieren von Skripten geändert hat. Mich würde interessieren, wie die Community darauf reagiert hat, etwa in Bezug auf UX, das Erlauben von Skripten oder den Missbrauch des
allow-Befehls. In der Python-Packaging-Community läuft mit Blick auf Wheel-Varianten gerade eine ähnliche Diskussion, und ich würde gern von den Erfahrungen anderer Ökosysteme lernen.Dieser Angriff hat sich inzwischen auf mehr als 180 Pakete ausgeweitet, siehe den Aikido-Security-Blog.
Ich frage mich, wer diesen Angriff ursprünglich entdeckt hat. Es ist interessant, dass verschiedene Blogs den Verdienst jeweils anders darstellen. Aikido sagt: „Wir haben einen groß angelegten Angriff entdeckt“, und auch Socket, Ox, Safety, Phoenix, Semgrep usw. beschreiben es jeweils anders.
Ich bin Mackenzie und arbeite bei Aikido. Die erste Person, die den Vorfall gemeldet hat, war der Entwickler Daniel Pereira. Er hat ihn an Socket weitergegeben, und Socket hat zuerst die 40 Pakete und den Schadcode analysiert. Danach hat Aikido zusätzlich 147 weitere Pakete sowie das Crowdstrike-Paket entdeckt. Tatsächlich war es Step, die zuerst erkannt haben, dass es sich bei dem Schadcode um einen sich selbst verbreitenden Wurm handelt. Interessant ist, dass mehrere Organisationen unabhängig voneinander unterschiedliche Rollen gespielt haben.
Offenbar haben mehrere Entwickler das fast zur gleichen Zeit entdeckt, und Step und Socket nennen jeweils unterschiedliche Personen. Letztlich haben Sicherheitsanbieter der Branche das mit jeweils eigenen Methoden erkannt, etwa per KI-Codeanalyse (Socket, Aikido) oder per eBPF-Pipeline-Monitoring (Step).
Wenn so viele Anbieter das unabhängig voneinander erkannt haben, fragt man sich, ob man diese Techniken nicht direkt an npm weitergeben könnte, damit die Registrierung bösartiger Pakete von vornherein blockiert wird. Dann könnten die Anbieter kein Frühwarnsystem mehr verkaufen, daher geben sie es wohl nicht heraus.
Der OP-Artikel zitiert direkt: „@franky47 entdeckte dieses Phänomen und meldete es der Community umgehend über ein GitHub-Issue.“
Ich finde den vom Angreifer gewählten Namen „Shai Hulud“ ziemlich geistreich: Der Name eines riesigen Wurms für echte Wurm-Malware. Auch die zentrale
bundle.jsist mit 3,6 MB riesig. Sogar die Malware-Variante ist ganz npm-typisch extrem aufgebläht.Ich habe das Gefühl, dass bald einmal ein Supply-Chain-Angriff zufällig einen anderen Supply-Chain-Angriff mit auslösen wird.
Auch Malware folgt dem Mooreschen Gesetz: Der tequila virus war 1991 2,6 KB groß, heute sind es mehrere MB.