npm install sollte nicht bedeuten: „Fremden Code auf meinem Computer ausführen“

Du gibst im Terminal ein:

npm install

oder:

pnpm install

Du erwartest, die von der Anwendung benötigten Bibliotheken herunterzuladen. Viele Jahre lang bedeutete dieser Vorgang im Node.js-Ökosystem jedoch mehr: Code anderer Personen herunterladen und einen Teil dieses Codes automatisch auf meinem Computer ausführen.

Diese Unterscheidung ist wichtig. Eine Datei herunterzuladen und ein Programm auszuführen, sind zwei unterschiedliche Vertrauensentscheidungen.

Seit 2026 blockiert npm 12 Installationsskripte von Abhängigkeiten standardmäßig, bis das Projekt sie ausdrücklich genehmigt. Das ist eine große und notwendige Änderung. Ältere npm-Versionen, andere Werkzeuge sowie spätere Befehle wie build oder test können jedoch weiterhin Code aus Abhängigkeiten ausführen. Das Problem ist nicht verschwunden — es ist lediglich besser sichtbar geworden. Die npm-Dokumentation beschreibt den aktuellen Mechanismus allowScripts, und GitHub erläutert die Änderung des Standardverhaltens in npm 12.

Was wird während der Installation tatsächlich ausgeführt?

Ein Paket kann Lifecycle-Skripte enthalten, unter anderem:

preinstall
install
postinstall

Legitime Pakete verwenden sie beispielsweise, um ein in C++ geschriebenes Add-on zu kompilieren, eine für das System geeignete Binärdatei herunterzuladen oder Ressourcen vorzubereiten.

Technisch gesehen ist ein solcher Eintrag:

{
  "scripts": {
    "postinstall": "node setup.js"
  }
}

jedoch keine harmlose Konfigurationsanweisung. Es ist ein Befehl zum Starten des Programms setup.js.

Wenn das Programm direkt auf dem Laptop ausgeführt wird, erhält es die Berechtigungen des angemeldeten Benutzers. Es kann versuchen, Dateien zu lesen, weitere Prozesse zu starten und sich mit dem Internet zu verbinden. Interessante Ziele sind beispielsweise:

~/.npmrc             Tokens zum Veröffentlichen von Paketen
~/.ssh/              SSH-Schlüssel
~/.aws/              AWS-Zugangsdaten
~/.kube/config       Zugriff auf Kubernetes-Cluster
.git-credentials     gespeicherte Git-Zugangsdaten

Auf dem Computer eines Entwicklers kann dies den Diebstahl von Code und Konten bedeuten. Auf einem CI-Server kann der Schaden größer sein, da der Runner möglicherweise Schlüssel für das Paket-Registry, Repositories, die Cloud und die Produktionsumgebung besitzt.

Dies ist keine Schwachstelle mit einer einzelnen CVE-Nummer. Es ist eine riskante Vertrauensgrenze: Fremder Code erhält die Möglichkeit, mit den Berechtigungen des Hosts zu arbeiten.

Vier Vorfälle, die die Eskalation des Problems zeigen

1. ESLint, 2018: vom Paket zum Veröffentlichungstoken

Im Juli 2018 übernahm ein Angreifer das Konto eines ESLint-Maintainers und veröffentlichte bösartige Versionen:

eslint-scope@3.7.2
eslint-config-eslint@5.0.2

Ihr postinstall lud Code von Pastebin herunter, führte ihn aus und sendete dem Angreifer den Inhalt der lokalen Datei .npmrc. Diese Datei enthielt häufig ein Token, mit dem weitere Pakete veröffentlicht werden konnten.

Die Folge hatte daher zwei Ebenen:

  1. Diebstahl eines Geheimnisses vom Computer des Opfers;
  2. die Möglichkeit, dieses Geheimnis zur Vergiftung weiterer Pakete zu nutzen.

npm widerrief Tokens, die vor dem Vorfall ausgestellt worden waren. Es gibt keine öffentlichen Belege dafür, dass gestohlene Tokens damals zur massenhaften weiteren Verbreitung des Angriffs verwendet wurden. Der Mechanismus selbst wurde jedoch sehr deutlich demonstriert. Offizieller ESLint-Postmortem.

Ein ungewöhnliches Detail: Laut Postmortem war für das Maintainer-Konto kein 2FA aktiviert, und sein Passwort war zuvor auch bei anderen Diensten verwendet worden. Eine Supply-Chain-Kompromittierung kann also nicht mit einem Fehler im Code beginnen, sondern mit einem wiederverwendeten Passwort.

2. ua-parser-js, 2021: XMRig und DanaBot auf Entwicklerrechnern

Im Oktober 2021 wurde das Konto des Autors eines beliebten ua-parser-js kompromittiert. Bösartige Versionen von 0.7.29, 0.8.0 und 1.0.0 enthielten ein preinstall.js-Skript, das das Betriebssystem erkannte und die passende Payload auswählte.

Der Angriff installierte:

  • XMRig — einen Monero-Kryptowährungs-Miner, der die Prozessorressourcen des Opfers verbraucht;
  • DanaBot unter Windows — einen Banking-Trojaner und Zugangsdaten-Dieb.

Dies war kein „bösartiges JavaScript-Fragment, das in einer Anwendung ausgeführt wird“. Das Installationsskript lud ein Programm auf Betriebssystemebene herunter und führte es aus. Mandiant verfolgte diesen Vorfall als Aktivität des Clusters UNC3379. Die Mandiant-Analyse beschreibt beide Payloads.

GitHub empfahl, ein Gerät, auf dem eine bösartige Version installiert wurde, als vollständig kompromittiert zu behandeln: das Paket entfernen, aber auch alle Schlüssel und Geheimnisse von einem anderen, sauberen Computer aus rotieren. Allein npm uninstall entfernt nicht das Programm, das das Paket bereits installiert hat. GitHub Advisory: CVE-2021-4229 / GHSA-pjwm-rvh2-c87w.

3. coa und rc, 2021: Passwörter, Screenshots und Keylogger

Einige Wochen später wurden die Pakete coa und rc kompromittiert. Das bösartige preinstall startete unter Windows eine Kette aus compile.js, einer Batch-Datei und einer heruntergeladenen DLL, die durch das systemeigene regsvr32.exe geladen wurde.

Die DLL:

  • stahl Passwörter aus Browsern;
  • suchte nach Zugangsdaten in FTP-, VNC- und E-Mail-Clients;
  • erstellte Screenshots;
  • protokollierte Tastatureingaben.

Dies ist ein Funktionsumfang, der für einen Credential Stealer in Kombination mit einem Keylogger typisch ist. Mandiant verknüpfte die Infrastruktur dieser Vorfälle mit DanaBot, während CERT-EU in seiner Mitteilung die Funktionen der DLL vorsichtig beschreibt, ohne den Namen der Malware-Familie abschließend festzulegen. CERT-EU SA2021-062 enthält Paketversionen, Dateien und weitere Kompromittierungsindikatoren.

4. Shai-Hulud, 2025–2026: Malware, die eigene Kopien veröffentlichte

Shai-Hulud veränderte das Ausmaß der Bedrohung. Es war ein Supply-Chain-Wurm: Er stahl nicht nur Tokens, sondern versuchte auch, diese zu verwenden, um infizierte Versionen weiterer Pakete zu veröffentlichen, die derselben Person gehörten.

Die erste Welle im Jahr 2025 nutzte hauptsächlich postinstall. Shai-Hulud suchte unter anderem nach npm- und GitHub-Tokens sowie Zugangsdaten für AWS, Azure und Google Cloud. Ein gestohlener Veröffentlichungstoken verwandelte ein Opfer in eine weitere Infektionsquelle. GitHub beschreibt diese Selbstreplikation und den mehrwelligen Charakter der Kampagne in der npm-Sicherheitszusammenfassung.

In Shai-Hulud 2.0 vom November 2025 begann die Kette früher, bei preinstall. Das Skript setup_bun.js prüfte, ob die Bun-Runtime verfügbar war, und installierte sie bei Bedarf. Anschließend führte es das eigentliche Modul zum Datendiebstahl aus.

Diese Variante konnte außerdem:

  • gestohlene Daten in GitHub-Repositories veröffentlichen;
  • ein Gerät als self-hosted GitHub-Actions-Runner registrieren und so einen Kanal für die spätere Ausführung von Befehlen schaffen;
  • weitere Pakete mithilfe von npm-Tokens infizieren;
  • unter bestimmten Bedingungen destruktive Befehle zum Löschen von Daten ausführen.

Datadog hat mindestens 796 Pakete und 1092 ihrer Versionen identifiziert, die Shai-Hulud 2.0 enthalten. Diese Zahlen beschreiben identifizierte Artefakte, nicht die Anzahl der Computer, auf denen die Malware mit Sicherheit ausgeführt wurde. Die Analyse von Datadog Security Labs stellt zudem eine Liste von Kompromittierungsindikatoren bereit.

Im Mai 2026 beschrieb Microsoft eine weitere Variante, Mini Shai-Hulud: mehr als 170 npm-Pakete, zwei PyPI-Pakete und insgesamt 404 bösartige Versionen. Bemerkenswert war ein Mechanismus zur Verschleierung eines Fehlers: Eine optionale bösartige Abhängigkeit führte den Payload aus und beendete sich anschließend absichtlich mit einem Fehler. npm konnte dies als fehlgeschlagene Installation eines optionalen Zusatzes behandeln und fortfahren, während der Code bereits ausgeführt worden war. Die Malware fügte außerdem einen Persistenzmechanismus zur Konfiguration von Claude Code hinzu. Analyse von Microsoft Security.

Was diese Vorfälle belegen — und was nicht

Sie belegen, dass ein auf dem Host ausgeführtes Abhängigkeitsskript alles nutzen kann, worauf der Benutzer oder CI-Runner Zugriff hat.

Sie belegen nicht, dass jedes Installationsskript bösartig ist. Sie belegen auch nicht, dass das Deaktivieren von postinstall das Problem löst. Abhängigkeitscode wird auch von Bundlern, Codegeneratoren, Test-Runnern und Befehlen wie den folgenden ausgeführt:

npm run build

Die richtige Frage lautet daher nicht: „Hat dieses Projekt postinstall?“. Sie lautet: Welchen fremden Code führen wir aus und worauf wird er Zugriff haben?

Git zeigt einen besseren Standardwert: clone != execute

Git verfügt ebenfalls über Hooks, also Programme, die bei Ereignissen wie Commit, Checkout oder Push ausgeführt werden. Traditionell befinden sie sich im lokalen .git/hooks/.

Der entscheidende Unterschied besteht darin, dass aktive Hooks und lokale Konfiguration keine gewöhnlichen, versionierten Inhalte sind, die durch git clone kopiert werden. Die Git-Dokumentation im Abschnitt SECURITY sagt ausdrücklich: Konfiguration und Hooks können beliebige Befehle ausführen, werden jedoch beim Klonen nicht kopiert; daher ist das bloße Klonen und Durchsehen nicht vertrauenswürdiger Inhalte grundsätzlich sicher.

„Grundsätzlich“ bedeutet nicht „immer“. Es sollten keine Befehle in einem .git-Verzeichnis ausgeführt werden, das direkt von einer nicht vertrauenswürdigen Person stammt, und Implementierungsfehler können diese Grenze durchbrechen.

Bereits 2006 unterschied Linus Torvalds in einer Antwort auf eine Diskussion mit dem Titel „Security problem“ zwischen der aus dem Repository heruntergeladenen Objektdatenbank und dem lokalen Zustand von Checkout und Index. Dies ist kein Beweis dafür, dass er moderne Angriffe auf Package Manager vorhergesehen hat. Es ist jedoch ein frühes Beispiel für die bewusste Trennung transportierter Daten vom lokalen Ausführungszustand. Archiv der Diskussion auf der Git-Mailingliste.

CVE-2024-32002: Path Traversal verwandelte Clone in RCE

Im Jahr 2024 wurde die kritische Schwachstelle CVE-2024-32002 entdeckt, die unter anderem als CWE-22: Path Traversal klassifiziert wurde.

Ein speziell präpariertes Repository mit Submodulen konnte Git auf einem Dateisystem, das nicht zwischen Groß- und Kleinschreibung unterscheidet und Symlinks unterstützt, täuschen. Anstatt eine Datei im Arbeitsverzeichnis des Submoduls zu speichern, platzierte Git einen ausführbaren Hook innerhalb von .git/. Der Hook wurde noch während Folgendem ausgeführt:

git clone --recurse-submodules ...

Die Folge war RCE — Remote Code Execution, also die Ausführung von Befehlen auf dem Computer des Opfers, die vom Autor des bösartigen Repositorys ausgewählt wurden, bevor der Benutzer Zeit hatte, den Code anzusehen.

Das offizielle Advisory bewertete die Schwachstelle mit 9,0/10 und nennt behobene Versionen, darunter Git 2.45.1, 2.44.1 und 2.43.4. GHSA-8h77-4q3w-gfgv / CVE-2024-32002.

Diese Schwachstelle war gerade deshalb gefährlich, weil sie die normale Git-Regel umging: Ein Remote-Repository sollte keine lokalen Hooks installieren können.

CVE-2026-52726 in Dulwich: dieselbe Grenze, anderer Fehler

Im Jahr 2026 wurde ein ähnliches Problem in Dulwich gefunden, einer in Python geschriebenen Implementierung von Git-Formaten und -Protokollen.

CVE-2026-52726 ist ebenfalls ein Path Traversal (CWE-22). Die Funktionen submodule_update() und clone(..., recurse_submodules=True) prüften den Submodulpfad unzureichend. Ein bösartiges .gitmodules konnte buchstäblich auf Folgendes als Ziel verweisen:

.git/hooks

Dulwich schrieb dort die Dateien des Angreifers und behielt deren Ausführungsbits bei. Eine spätere Git- oder Dulwich-Operation, die den entsprechenden Hook auslöste, führte zu RCE.

Im Unterschied zu CVE-2024-32002 erforderte dieser Angriff kein Dateisystem ohne Unterscheidung von Groß- und Kleinschreibung: Der literale Pfad .git/hooks funktionierte unter Linux, macOS und Windows. Betroffen waren Versionen von 0.23.2 bis 1.2.4; der Fix wurde in Dulwich 1.2.5 veröffentlicht. GHSA-gfhv-vqv2-4544 / CVE-2026-52726.

Beide CVEs zeigen dieselbe Regel: Ein Angreifer benötigt häufig keinen neuen Ausführungsmechanismus. Es genügt, seine Datei an einem Ort abzulegen, den das System bereits als berechtigt zur Codeausführung betrachtet.

Wenn Code ausgeführt werden muss, begrenzen wir seinen Spielraum

Das Bauen von Anwendungen erfordert das Ausführen von Code. Das Risiko lässt sich nicht auf den Slogan „alle Skripte deaktivieren“ reduzieren. Der potenzielle Schaden lässt sich jedoch begrenzen.

In GoatCMS kann die Installations- und Build-Aufgabe in einem isolierten Container ausgeführt werden, zum Beispiel:

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
WEBEOF

Die Befehle bleiben gleich. Die Prozessumgebung ändert sich. Eine Abhängigkeit muss weder das Home-Verzeichnis des Hosts noch dessen SSH-Schlüssel oder Cloud-Konfiguration sehen. Sie erhält nur die Ressourcen, die der Aufgabe ausdrücklich übergeben wurden.

Dies beschreibt das Sicherheitsmodell von GoatCMS und ist keine Behauptung, dass jeder Container automatisch vollständige Isolation bietet.

Ein Container kann ein Hintertürchen in der Größe eines Tores haben

Eine solche Ausführung bleibt riskant:

docker run \
  --privileged \
  -v /:/host \
  -v /var/run/docker.sock:/var/run/docker.sock \
  ...

--privileged erweitert die Fähigkeiten des Prozesses. Das Mounten von / stellt das Dateisystem des Hosts bereit. Der Docker-Socket erlaubt es, Befehle an den Daemon zu senden, der weitere Container erstellen und Host-Dateien einhängen kann. In der Praxis ist dies häufig ein Weg zu sehr weitreichender Kontrolle über die Maschine. Die Sicherheitsdokumentation von Docker Engine beschreibt die Bedeutung des Schutzes des Zugriffs auf den Daemon.

Ein Container begrenzt den Schaden nur dann, wenn wir seine Fähigkeiten tatsächlich einschränken:

  • wir mounten ausschließlich benötigte Verzeichnisse;
  • wir übergeben nur notwendige, kurzlebige Secrets;
  • wir verwenden --privileged nicht ohne Notwendigkeit;
  • wir mounten den Docker-Socket nicht;
  • wir beschränken ausgehenden Verkehr, wenn der Build keinen Internetzugang benötigt;
  • wir entfernen die Umgebung nach Ausführung der Aufgabe;
  • wir pinnen das Image auf einen konkreten Digest, wenn wir Reproduzierbarkeit benötigen.

Ein Lockfile hilft, reicht aber nicht aus

Die Lockdatei speichert die exakten Versionen der Abhängigkeiten. Dadurch sollte eine neue, bösartige Version nicht allein deshalb heruntergeladen werden, weil sie in den breiten Versionsbereich aus package.json fällt.

Dies ist jedoch kein Sicherheitszertifikat. Wenn eine bösartige Version bereits akzeptiert und in der Lockdatei gespeichert wurde, werden nachfolgende Installationen sie sehr zuverlässig reproduzieren.

Es lohnt sich, mehrere Ebenen zu kombinieren:

  1. angeheftete Abhängigkeiten und obligatorisches Review von Lockdatei-Änderungen;
  2. explizite Genehmigung von Installationsskripten;
  3. phishing-resistentes MFA und Veröffentlichung über OIDC statt langlebiger Tokens;
  4. isolierte, einmalige Build-Umgebungen;
  5. minimale Mounts, Berechtigungen und Secrets;
  6. Netzwerkeinschränkungen und Überwachung von Exfiltrationsversuchen;
  7. Prüfung der Herkunft des Artefakts, nicht nur des Links zum Repository.

Ein interessantes Signal für den Wandel in der Branche: npm 12 blockiert Abhängigkeitsskripte ohne Genehmigung. Zuvor schloss selbst --ignore-scripts nicht alle Wege. Eine aus Git installierte Abhängigkeit konnte .npmrc enthalten, das das als Git verwendete Programm ersetzte und die Codeausführung über einen anderen Pfad erreichte. Deshalb wurde in npm 11 --allow-git eingeführt, und npm 12 beschränkte standardmäßig auch Git-Abhängigkeiten. Beschreibung des Mechanismus --allow-git.

Frage nicht nur, ob du dem Autor vertraust

Der Autor eines Pakets muss keine bösen Absichten haben. Ein kompromittiertes Konto, ein gestohlener Token, ein anfälliger Veröffentlichungsprozess oder eine kompromittierte transitive Abhängigkeit reichen aus.

Die Frage „Ist dieser Maintainer vertrauenswürdig?“ ist daher zu eng gefasst. Besser ist:

Was wird dieser Code tun können, wenn das Konto des Autors morgen kompromittiert wird?

Git fasst einen guten Standardwert in einer einfachen Regel zusammen:

clone != execute

Build-Systeme benötigen ähnliche Grenzen:

download != trust
install != Zugriff auf den Host
build != uneingeschränkte Berechtigungen

Isolation wird bösartigen Code nicht sicher machen. Sie kann jedoch verhindern, dass ein kompromittiertes Paket gleichzeitig den Laptop des Entwicklers, die Cloud-Schlüssel und den gesamten Veröffentlichungsprozess übernimmt.

Das ist das eigentliche Ziel containerisierter Skripte in GoatCMS: nicht das Versprechen, dass jede Abhängigkeit ehrlich ist, sondern die Annahme, dass eine es irgendwann nicht sein wird — und die Vorbereitung des Systems auf diesen Tag.

Quellen

npm und Supply-Chain-Vorfälle

Git, Schwachstellen und Isolation