SSD Nodes Learn Hosting plans →
Anleitungen Matt ConnorVon Matt Connor · Aktualisiert 2026-08-27

npm-Lieferkettenangriffe auf dem eigenen Server verhindern

So erreichen bösartige npm-Versionen Ihre Node-App auf dem VPS: Patch-Releases, postinstall-Skripte und Typosquats erklärt, plus sichere Deploy-Praxis.

Was ein npm-Lieferkettenangriff auf Ihrem Server ist

Ein npm-Lieferkettenangriff erreicht Ihren Server über ein Paket, das Sie zur Installation ausgewählt haben. Dabei ist kein offener Port und kein Exploit-Schritt erforderlich. npm (Node Package Manager) installiert Code. Durch die Installation von Code wird Code ausgeführt. Deshalb bindet eine kleine Node-Anwendung mehrere hundert Pakete ein, die Sie nie geprüft haben. Jedes dieser Pakete kann eine Stunde später eine neue Version veröffentlichen.

Ihr Deployment ruft eine bösartige Version ab, weil Ihr Installationsbefehl die neueste passende Version angefordert hat. Dieser Code läuft anschließend mit den Berechtigungen der Person, die die Installation ausgeführt hat. Alles Folgende ergibt sich aus diesen beiden Sätzen.

Die Varianten sind danach geordnet, wie häufig sie eine einzelne Person treffen, die eine Node-Anwendung auf einem einzelnen VPS bereitstellt. Ein großes Unternehmen würde diese Reihenfolge nicht verwenden. Es verfügt über eine interne Registry, ein Review-Team und einen Mirror der öffentlichen Registry. Sie haben ein Deploy-Skript.

Form 1: Ein Maintainer-Konto wird kompromittiert und veröffentlicht einen Patch

Die npm-Registry erlaubt es niemandem, den Inhalt einer bereits vorhandenen Version zu ändern. Ein Angreifer, der einen Maintainer per Phishing täuscht oder ein Publish-Token stiehlt, kann 4.18.2 daher nicht überschreiben. Er veröffentlicht 4.18.3.

Prüfen Sie Ihre package.json. Eine Zeile wie "express": "^4.18.2" bedeutet nicht Version 4.18.2. Das Caret-Zeichen bedeutet „jede 4.x-Version ab dieser Version“, und ~4.18.2 bedeutet „jede 4.18.x-Version“. npm install löst diesen Bereich beim Ausführen auf. Dadurch können beim zweimaligen Deployen desselben Git-Commits am selben Nachmittag zwei unterschiedliche Codesätze installiert werden. Diese Lücke ist die Angriffsfläche. Auf Ihrem Computer muss nichts kompromittiert sein, damit sie entsteht.

Schädliche Releases werden normalerweise gemeldet und zurückgezogen. Das geschieht jedoch erst, nachdem sie installiert wurden. Wer während dieses Zeitfensters ein Deployment durchgeführt hat, hat den Code auf der Festplatte. Eine Pipeline, die bei jedem Lauf Versionsbereiche auflöst, gerät automatisch in dieses Zeitfenster. Das kann mehrmals pro Woche geschehen, ohne dass jemand dies bewusst entscheidet.

Form 2: Ein Installationsskript läuft unter dem Benutzerkonto, das die Bereitstellung ausführt

Der package.json eines Pakets kann in seinem scripts-Block preinstall, install, postinstall und prepare deklarieren. npm führt diese Skripte während der Installation aus. Sie laufen nicht in einer Sandbox, und niemand prüft sie. Es handelt sich um Shell-Befehle, die unter dem Benutzerkonto ausgeführt werden, das den Installationsbefehl eingegeben hat, in dessen Home-Verzeichnis, mit dessen Netzwerkzugriff und mit der vollständigen Umgebung dieser Shell.

Die relevante Frage lautet daher nicht, was das Paket tun kann. Entscheidend ist, welche Daten dieser Benutzer lesen kann. Auf einem normalen Bereitstellungsserver gehören dazu ~/.npmrc mit einem Registry-Token, ~/.ssh/id_ed25519 als Deploy-Key für SSH (Secure Shell), ~/.aws/credentials, ~/.docker/config.json und jede exportierte Variable der Shell. Dort befindet sich üblicherweise auch DATABASE_URL.

Eine Payload wie diese benötigt weder Persistenz noch eine Rechteausweitung. Sie liest einige Dateien, sendet sie über HTTPS an einen Host und beendet sich mit Status 0. Sie sehen nichts, weil npm die Ausgabe von Installationsskripten standardmäßig verbirgt. Deaktivieren Sie dieses Verhalten und beobachten Sie, was tatsächlich ausgeführt wird:

npm ci --foreground-scripts

foreground-scripts verwendet dieselbe Standardeingabe sowie dieselbe Standardausgabe und Standardfehlerausgabe wie der npm-Prozess. Dadurch geben Build-Skripte ihre Ausgabe direkt in Ihrem Terminal aus, statt sie in einen Puffer zu schreiben, den npm bei erfolgreicher Installation verwirft.

Form 3: Typosquatting und der Name, den Sie nicht ganz eingegeben haben

Ein Typosquat ist ein Paket, das unter einem Namen veröffentlicht wird, der einem bekannten Namen ähnelt. Es wartet auf einen vertippten oder falsch eingefügten Installationsbefehl. Entscheidend ist der Befehl, nicht der Code. Eine Lock-Datei hilft Ihnen hier daher nicht: Sie fügen den falschen Namen einmal hinzu, und danach pinnt die Lock-Datei ihn zuverlässig.

Die Variante, die eher Teams als Einzelpersonen betrifft, ist Dependency Confusion. Ihr internes Paket heißt billing-utils und liegt in einer privaten Registry. Wenn in der öffentlichen Registry nichts mit dem Namen billing-utils existiert, kann ihn jeder veröffentlichen. npm löst Namen ohne Scope gegen die standardmäßige öffentliche Registry auf. Dadurch kann die öffentliche Kopie verwendet werden. Die Lösung ist ein Scope, den Sie kontrollieren, zusammen mit einer Registry-Zuordnung für diesen Scope in .npmrc:

@yourorg:registry=https://npm.yourorg.example/
//npm.yourorg.example/:_authToken=${NPM_TOKEN}

@yourorg/billing-utils wird jetzt ausschließlich von diesem Host abgerufen, weil die Zuordnung von Scope zu Registry vor der Standard-Registry ausgewertet wird. Ein interner Name ohne Scope hat keine Zuordnung und ist daher nicht geschützt.

Bevor Sie eine neue Abhängigkeit hinzufügen, prüfen Sie das Paket selbst und nicht nur das Download-Badge:

npm view some-lib repository.url maintainers time.created time.modified

Ein Paket, das im vergangenen Monat erstellt und von einem Konto veröffentlicht wurde, das sich keinem öffentlichen Repository zuordnen lässt, stellt ein anderes Risiko dar als ein Paket mit einer sechsjährigen Historie. Keine dieser Angaben ist ein Beweis. Beide lassen sich jedoch schnell prüfen.

Form 4: Der Verantwortliche einer Abhängigkeit hat unbemerkt gewechselt

Maintainer übergeben die Verantwortung für Pakete. Jemand gibt wegen Überlastung auf, eine unbekannte Person bietet ihre Hilfe an, Veröffentlichungsrechte wechseln den Besitzer, und kein Hinweis erreicht die Projekte, die davon abhängen. Es wurde nichts kompromittiert. Das Vertrauen, das Sie 2021 gewährt haben, liegt nun bei einer anderen Person.

Diese Form tritt am langsamsten auf und ist am schwersten zu erkennen. Kein Befehl beantwortet diese Frage direkt. Zwei Maßnahmen schränken das Risiko ein. Prüfen Sie vor der Übernahme eines Pakets, wer es veröffentlichen darf, und verwenden Sie dazu die oben stehende Zeile npm view. Lesen Sie anschließend den Diff, wenn sich ein Paket ändert, von dem Sie tatsächlich abhängen:

npm diff --diff-name-only --diff=some-lib@1.4.2 --diff=some-lib@1.4.3
npm diff --diff=some-lib@1.4.2 --diff=some-lib@1.4.3

Die erste Form gibt nur die geänderten Dateinamen aus. Das ist schnell genug, um sie bei jedem Upgrade eines wichtigen Pakets auszuführen. Ein Patch-Release, das ein Build-Skript ändert, eine Datei im Paketstammverzeichnis hinzufügt oder den scripts-Block bearbeitet, sollten Sie vollständig lesen, bevor es Ihren Server erreicht.

Erstellen aus einer festgeschriebenen Lockfile mit npm ci

package-lock.json erfasst die exakte Version jedes Pakets im Abhängigkeitsbaum, die URL, von der es stammt, einen sha512-Integritätshash für jedes Tarball und das Paket, das es angefordert hat. Committen Sie diese Datei. Nur sie dokumentiert, was Sie tatsächlich getestet haben.

Installieren Sie anschließend mit npm ci, niemals mit npm install, auf Maschinen, die kein Entwickler-Laptop sind:

npm ci --omit=dev --ignore-scripts

npm ci unterscheidet sich in mehreren für diesen Fall wichtigen Punkten von npm install. Eine Lockfile muss vorhanden sein. Vor dem Start entfernt der Befehl jede vorhandene node_modules, sodass Reste eines früheren Deployments nicht in dieses Deployment übernommen werden können. Der Befehl schreibt niemals in package.json oder in die Lockfile. Eine Installation kann Sie daher nicht unbemerkt auf eine neuere Version bringen. Wenn die Lockfile und package.json nicht übereinstimmen, wird der Vorgang mit einem Fehler beendet, anstatt die Abweichung aufzulösen.

Dieser Fehler ist eine Funktion und kein Ärgernis. Er bedeutet, dass eine Änderung an einer Abhängigkeit als Commit eintreffen muss, den jemand geprüft hat, und nicht als Nebeneffekt eines Deployments um 02:00.

Der Integritätshash wird bei jedem Abruf geprüft. Ein Tarball, dessen Bytes nicht mit dem aufgezeichneten Hash übereinstimmen, bricht die Installation mit code EINTEGRITY ab, anstatt entpackt zu werden. Seien Sie präzise bei der Aussage darüber, was dadurch gewährleistet wird: Der Hash bestätigt, dass die empfangene Datei der Datei entspricht, auf die die Lockfile festgelegt ist. Das ist dieselbe Garantie, die Downloads mit Prüfsummen verifizieren bietet, und sie ist auf dieselbe Weise eingeschränkt. Der Hash sagt nichts darüber aus, ob die festgelegte Version bei ihrer Veröffentlichung bösartig war.

Ein Detail zu --omit=dev: Diese Pakete werden weiterhin aufgelöst und weiterhin in die Lockfile geschrieben. Sie werden lediglich nicht auf der Festplatte abgelegt. Weniger Pakete auf der Festplatte bedeuten weniger Installationsskripte und weniger Code, der zur Laufzeit geladen wird. Daher ist dieser Schritt sinnvoll. Eine Abhängigkeit wird dadurch nicht aus Ihrem Abhängigkeitsbaum entfernt.

Installationsskripte sind Code. Sie müssen sie ablehnen können.

Sie können Installationsskripte deaktivieren. Tragen Sie Folgendes in .npmrc des Projekts ein und committen Sie die Datei neben der Lockdatei:

ignore-scripts=true
save-exact=true

ignore-scripts=true verhindert, dass npm die in Abhängigkeiten deklarierten Skripte ausführt. save-exact=true sorgt dafür, dass npm install some-lib stattdessen 1.4.2 in package.json schreibt und nicht ^1.4.2. Dadurch gelangt ein auflösbarer Versionsbereich nicht versehentlich in Ihr Manifest.

Dadurch können Funktionen ausfallen. Sie sollten daher wissen, wie Sie damit umgehen, bevor Sie diese Einstellung aktivieren. Pakete, die ein natives Add-on kompilieren oder eine vorkompilierte Binärdatei herunterladen, erledigen diese Arbeit in einem Installationsskript. Bei deaktivierten Skripten ist die Installation selbst erfolgreich. Der Fehler tritt erst später zur Laufzeit auf, wenn ein Modul seine Binding-Datei nicht laden kann. Die Lösung ist eine Allowlist:

npm ci --ignore-scripts
npm rebuild better-sqlite3

npm rebuild <package> führt die Build-Skripte für dieses eine Paket aus. Sie treffen damit eine Entscheidung pro Paket, statt einigen hundert unbekannten Paketen pauschal die Ausführungsberechtigung zu erteilen.

Um zu sehen, wie groß diese Berechtigung aktuell ist, fragen Sie npm ab:

npm query ":attr(scripts, [postinstall])"

Damit werden alle Pakete im installierten Baum ausgegeben, die ein postinstall-Skript enthalten. Bei einer typischen Anwendung ist die Liste kürzer als erwartet. Genau das macht die Allowlist praktikabel.

Build und das Ausliefern von Netzwerkverkehr trennen

Der Deploy-Benutzer muss in node_modules schreiben können. Der Prozess, der HTTP-Anfragen beantwortet, benötigt diese Berechtigung nicht. Wenn beide denselben Account verwenden, kann Code, der während der Installation ausgeführt wird, den Code ändern, der Ihre Benutzer bedient. Auch Code, der zur Laufzeit ausgeführt wird, kann ihn ändern.

Trennen Sie die beiden Rollen. Erstellen Sie den Build als ein Benutzer, und lassen Sie die Anwendung als ein anderer Benutzer laufen. Das ausgelieferte Verzeichnis machen Sie für den Benutzer, unter dem die Anwendung läuft, schreibgeschützt:

sudo useradd --system --home-dir /srv/nodeapp --shell /usr/sbin/nologin nodeapp
sudo chown -R deploy:nodeapp /srv/nodeapp
sudo chmod -R o-rwx /srv/nodeapp

Lassen Sie anschließend systemd dies erzwingen. Schreiben Sie /etc/systemd/system/nodeapp.service:

[Unit]
Description=Node application
After=network-online.target

[Service]
User=nodeapp
Group=nodeapp
WorkingDirectory=/srv/nodeapp/current
EnvironmentFile=/etc/nodeapp/env
ExecStart=/usr/bin/node server.js
Restart=on-failure

NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/srv/nodeapp/shared
NoExecPaths=/srv/nodeapp/shared
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX

[Install]
WantedBy=multi-user.target

ProtectSystem=strict bindet das gesamte Dateisystem für diesen Dienst schreibgeschützt ein, ausgenommen /dev, /proc, /sys und alles, was Sie in ReadWritePaths angeben. Daher schlägt ein Versuch der Anwendung, in node_modules zu schreiben, mit EROFS: read-only file system fehl. Sie können dies innerhalb von etwa einer Minute in Ihren eigenen Logs reproduzieren. NoExecPaths betrifft das beschreibbare Upload-Verzeichnis: Der Dienst kann dort Dateien schreiben, aber der Kernel verweigert deren Ausführung. Diese Option benötigt systemd 249 oder neuer. Ubuntu 24.04 wird mit Version 255 ausgeliefert.

Diese Unit-Datei enthält zwei typische Fehlerquellen. Fügen Sie erstens nicht MemoryDenyWriteExecute=yes hinzu. Diese Option steht in den meisten Hardening-Listen für systemd. Sie verhindert jedoch den Start von Node, weil V8 JavaScript zur Laufzeit in Maschinencode kompiliert und Seiten benötigt, die gleichzeitig beschreibbar und ausführbar sind. Übernehmen Sie zweitens den Pfad ExecStart aus command -v node. Wurde Node mit einem Versionsmanager installiert, liegt es im Home-Verzeichnis des Deploy-Benutzers. ProtectHome=yes verbirgt dieses Verzeichnis anschließend vor dem Dienst. Die Unit schlägt dann sofort mit status=203/EXEC fehl, und im Log steht, dass die ausführbare Datei nicht gefunden werden konnte.

Prüfen Sie das Ergebnis, statt der Datei zu vertrauen:

sudo systemctl daemon-reload
sudo systemctl enable --now nodeapp
sudo systemd-analyze security nodeapp.service
sudo -u nodeapp touch /srv/nodeapp/current/probe

systemd-analyze security listet jede Hardening-Einstellung zusammen mit ihrer Angriffsfläche auf. So sehen Sie, welche Einstellungen noch den Standardwert verwenden. touch sollte mit Permission denied fehlschlagen, weil nodeapp nichts unter current besitzt. Wenn der Befehl erfolgreich ist, sind die Dateieigentümer falsch. Dann kompensieren die systemd-Einstellungen diesen Fehler unbemerkt.

Ein Hinweis zu EnvironmentFile: systemd liest die Datei als root ein, bevor es zu User=nodeapp wechselt. Daher kann die Datei mit den Rechten 600 mit root:root geschützt werden. Die Anwendung erhält die Variablen trotzdem. Jeder, der eine Shell als nodeapp besitzt, kann sie weiterhin aus /proc/<pid>/environ lesen. Dies schützt das Secret also im Ruhezustand, nicht aber den laufenden Prozess.

Build-Authentifizierungsdaten aus der Build-Umgebung heraushalten

Installationsskripte übernehmen die Umgebung. Allein diese Tatsache sollte bestimmen, wo Sie bauen.

Am sichersten ist es, an einem Ort zu bauen, der nicht der Produktionsserver ist, und das fertige Verzeichnis anschließend zu übertragen. Auf der Build-Maschine liegt dann nur ein schreibgeschütztes Registry-Token. Es gibt dort keinen SSH-Deploy-Key, keinen Cloud-Zugriffsschlüssel, kein Datenbankpasswort und keine Anmeldung an einer Container-Registry.

npm token create --read-only

Ein schreibgeschütztes Token kann Pakete abrufen, aber nichts veröffentlichen. Wird es aus einer Build-Umgebung gestohlen, besteht der Schaden darin, dass öffentliche Pakete heruntergeladen werden können.

Wenn Sie auf dem Server bauen müssen, führen Sie den Build als Benutzer deploy mit einer bewusst eingeschränkten Umgebung aus. Bewahren Sie die Laufzeitgeheimnisse in /etc/nodeapp/env auf. deploy darf darauf nicht zugreifen können. Dasselbe gilt für Build-Automatisierung, die Sie selbst hosten: Ein selbst gehosteter GitHub-Actions-Runner enthält Tokens und führt bei jedem Job beliebigen veröffentlichten Code aus. Damit ist er die wertvollste Maschine in einer kleinen Bereitstellung. Jedes Programm, das Sie nicht selbst geschrieben haben und das Ihre vollständige Umgebung erhält, gehört in dieselbe Kategorie. Deshalb ist das Heraushalten von Geheimnissen aus der Umgebung eines KI-Agenten dasselbe Problem mit einem anderen Programm dazwischen.

Pakete festlegen oder vendorisieren, die Sie nicht prüfen können

Eine festgelegte Abhängigkeit ist eine Abhängigkeit, deren Version sich nicht ohne einen Commit ändern kann. Die versionierte Lockdatei bewirkt das bereits für den gesamten Abhängigkeitsbaum. In zwei Fällen sind zusätzliche Maßnahmen erforderlich.

Transitive Abhängigkeiten sind der erste Fall. Sie kontrollieren nicht, wovon Ihre Abhängigkeiten abhängen. Mit overrides in package.json erzwingen Sie eine Version an jeder Stelle im Abhängigkeitsbaum:

{
  "overrides": {
    "some-transitive-lib": "1.4.2"
  }
}

Führen Sie npm install einmal nach dem Hinzufügen aus, damit die Lockdatei das Ergebnis erfasst, und committen Sie anschließend beide Dateien.

Der zweite Fall ist ein Paket, das Sie nicht prüfen und nicht entfernen können. Vendorisieren Sie es. npm pack lädt genau das Tarball herunter, das die Registry ausliefern würde, und eine file:-Abhängigkeit installiert es aus Ihrer Kopie:

npm pack some-lib@1.4.2
mkdir -p vendor && mv some-lib-1.4.2.tgz vendor/
{
  "dependencies": {
    "some-lib": "file:vendor/some-lib-1.4.2.tgz"
  }
}

Das Tarball liegt nun in Ihrem Repository und kann nicht unbemerkt geändert werden. Sie übernehmen damit jedoch dauerhaft auch seine Aktualisierungen. Verwenden Sie dieses Verfahren daher für ein kleines, aufgegebenes Paket, auf das Sie angewiesen sind, nicht für Ihr Webframework.

Es gibt außerdem eine Abkühlfrist, die nichts kostet:

npm install --before=2026-08-01

Die Option before erstellt den Abhängigkeitsbaum ausschließlich mit Versionen neu, die an diesem Datum oder früher veröffentlicht wurden. Setzen Sie beim Aktualisieren der Abhängigkeiten ein Datum ein oder zwei Wochen zurück. Dadurch überspringen Sie das Zeitfenster, in dem eine fehlerhafte Version verfügbar, aber noch nicht gemeldet ist. Das Verfahren ist grob, weil dadurch auch echte Sicherheitsupdates zurückgehalten werden. Verwenden Sie es, um die Versionsbereiche aufzulösen, prüfen Sie die Änderungen und committen Sie anschließend die Lockdatei. Derselbe Grundsatz gilt für die Kommandozeilenwerkzeuge, die Sie über npm installieren, statt sie als Abhängigkeit einzubinden: Ein nicht festgelegter npx-Aufruf lädt, was an diesem Morgen veröffentlicht wurde. Die Festlegung einer exakten dsh-Version sorgt dagegen dafür, dass zwei Maschinen denselben Code ausführen.

Woran erkenne ich, welche Version tatsächlich ausgeliefert wurde?

Die Lockdatei in git zeigt, was installiert werden sollte. Der Datenträger zeigt, was installiert ist. Nur Letzteres ist ein Beleg.

npm ls some-lib
node -e "console.log(require('./node_modules/some-lib/package.json').version)"

npm ls liest node_modules und meldet daher, was physisch vorhanden ist, statt die Vorgaben der Lockdatei zu verwenden. Die Zeile mit node -e liest das installierte Manifest anhand seines Pfads. Das funktioniert auch bei Paketen, deren Feld exports den Import von Unterpfaden verhindert. Außerdem wird genau eine Version ausgegeben, ohne eine Baumdarstellung darum herum.

Für die andere Hälfte des Vergleichs lesen Sie git:

git log --oneline -- package-lock.json
git show <commit>:package-lock.json | grep -A3 '"node_modules/some-lib"'

Stellen Sie die Verbindung zwischen beiden dauerhaft her, indem Sie den Commit in das Deployment-Layout aufnehmen. Veröffentlichen Sie nach /srv/nodeapp/releases/<short commit sha> und verweisen Sie mit einem symbolischen Link von /srv/nodeapp/current darauf. Die Antwort auf die Frage „Was läuft gerade?“ lautet dann readlink /srv/nodeapp/current. Diese Information ist auch um 03:00 für jemanden verfügbar, der das Deployment nicht durchgeführt hat.

Prüfen Sie abschließend, wofür die Registry bürgt:

npm audit signatures

Damit werden die Registry-Signaturen der Pakete in Ihrem installierten Baum geprüft. Außerdem werden Provenance-Attestierungen für Pakete geprüft, die solche Attestierungen besitzen. Provenance verknüpft ein veröffentlichtes Tarball mit dem öffentlichen Continuous-Integration-(CI-)Build, der es erzeugt hat. Eine verifizierte Attestierung bedeutet daher, dass Sie den Code bis zu einem Commit zurückverfolgen können und nicht nur bis zu einem unbekannten Laptop. Die Abdeckung ist nicht vollständig. Eine fehlende Attestierung bedeutet daher „keine Information“ und nicht „fehlerhaftes Paket“.

Was nach einer fehlerhaften Version auf Ihrem Server zu tun ist

Gehen Sie von dem aus, was ausgeführt wurde, und ermitteln Sie, unter welchem Benutzer dies geschah.

Wurde der Code während der Installation ausgeführt, müssen Sie davon ausgehen, dass alle für den Build-Benutzer lesbaren Daten offengelegt wurden. Erneuern Sie das Registry-Token, die SSH-Schlüssel im Home-Verzeichnis dieses Benutzers, die Cloud-Anmeldedaten und jedes Secret, das in dieser Shell exportiert wurde. Eine Erneuerung ist die einzig vertretbare Reaktion, weil Sie nicht nachweisen können, dass eine Datei nicht gelesen wurde.

Wurde der Code zur Laufzeit unter einem eingeschränkten Dienstkonto ausgeführt, ist die erreichbare Menge deutlich kleiner: die Umgebungsvariablen der Anwendung selbst und alles, was über ihren Netzwerkzugriff erreichbar ist. Genau deshalb sollten Sie Dienste als nicht privilegierte Benutzer auf einem VPS ausführen. Das verhindert den Sicherheitsvorfall nicht. Es bestimmt, auf welchen Teil des Systems sich der Sicherheitsvorfall auswirkt und ob er einen Neustart übersteht.

Erstellen Sie das System anschließend neu, statt es zu bereinigen. Löschen Sie node_modules, pinnen Sie das betroffene Paket in package.json auf eine Version unterhalb der fehlerhaften Version, führen Sie einmal npm install aus, um die Lock-Datei zu aktualisieren, committen Sie die Änderung und stellen Sie mit npm ci bereit. Reparieren Sie keinen bestehenden Verzeichnisbaum. Sie können nicht vollständig ermitteln, welche Dateien und Einstellungen ein Installationsskript verändert hat.

Dokumentieren Sie außerdem das Zeitfenster: das erste Deployment, das die Version hätte beziehen können, und das Deployment, mit dem sie entfernt wurde. Anhand dieses Bereichs können Sie bestimmen, welche Ihrer eigenen Logs geprüft werden müssen. Das ist nur möglich, wenn Ihre Releases nach Commits benannt sind.

Was all das nicht behebt

Eine Lockdatei macht eine Abhängigkeit nicht sicher. Sie macht aus dem Zeitpunkt, an dem Sie diese Abhängigkeit akzeptiert haben, eine datierte und überprüfte Entscheidung statt eines Nebeneffekts eines Deployments. Jede der oben beschriebenen Maßnahmen vollzieht dieselbe Umwandlung: aus einem Zufall wird eine bewusste Entscheidung.

npm audit ist hier keine Schutzmaßnahme. Das Tool vergleicht Ihren Abhängigkeitsbaum mit einer Datenbank gemeldeter Sicherheitslücken. Dadurch findet es Probleme, die bereits veröffentlicht und benannt wurden. Ein Supply-Chain-Angriff bleibt während seiner gesamten wirksamen Lebensdauer unbenannt. Führen Sie npm audit für alte, bekannte Fehler aus. Erwarten Sie davon keine Aussage über ein Release, das vor vier Stunden veröffentlicht wurde.

Die Anzahl Ihrer Abhängigkeiten zu reduzieren hilft mehr als jedes Tool in diesem Leitfaden. Trotzdem ist dies der am wenigsten beliebte Rat. Jedes Paket, das Sie nicht hinzufügen, bedeutet einen weiteren Publisher, der nicht in Ihrem Namen per Phishing angegriffen werden kann. Außerdem wird ein weiteres Installationsskript nie als Ihr Deploy-Benutzer ausgeführt.

Nichts davon ist spezifisch für npm. Dieselben vier Formen gelten auch für PyPI, RubyGems, Container-Images und den Paketmanager Ihrer Distribution. Bei npm tritt das Problem am deutlichsten auf, weil die Abhängigkeitsbäume tiefer sind und Installationsskripte standardmäßig ausgeführt werden. Alles, was ein bereits von Ihnen eingesetztes Tool erweitert, übernimmt dasselbe Problem. Deshalb ist die Ermittlung, worauf ein dsh-Plugin vor der Installation zugreifen kann dieselbe Prüfung wie das Lesen eines postinstall-Skripts, wobei die Berechtigungen Ihres Agents an die Stelle der Berechtigungen Ihres Deploy-Benutzers treten. Wie viel von der umgebenden Maschine Sie schützen müssen, hängt vom Ausführungsort ab. Das ist Teil der grundsätzlicheren Frage, ob VPS-Hosting sicher ist.

FAQ

Schützt mich npm ci vor einem kompromittierten npm-Paket?

Es schützt Sie davor, dass sich die Version ohne Ihr Wissen ändert. npm ci installiert genau das, was package-lock.json festhält, prüft jedes Tarball anhand seines sha512-Integritätshashs und beendet sich mit einem Fehler, wenn package.json und die Lockdatei nicht übereinstimmen, statt die Abweichung selbstständig aufzulösen. Ob die festgelegte Version sicher ist, sagt das nicht aus. Wenn Sie eine Lockdatei committen, die eine bösartige Version festlegt, installiert npm ci diese Version jedes Mal zuverlässig auf jedem Server, den Sie betreiben.

Sollte ich ignore-scripts=true für alles setzen?

Setzen Sie die Option und erstellen Sie anschließend eine Allowlist. ignore-scripts=true in der .npmrc des Projekts verhindert die Ausführung von Installationsskripten für Abhängigkeiten. Dadurch entfällt der direkteste Weg von einem schädlichen Paket zu den Zugangsdaten Ihres Deploy-Benutzers. Pakete, die ein natives Add-on kompilieren oder eine vorkompilierte Binärdatei abrufen, benötigen ihre Skripte tatsächlich. Sind Skripte deaktiviert, schlagen diese Pakete später zur Laufzeit wegen einer fehlenden Binding-Datei fehl, statt bereits bei der Installation. Führen Sie npm ci --ignore-scripts aus und anschließend npm rebuild <package> für die wenigen Pakete, denen Sie vertrauen. npm query ":attr(scripts, [postinstall])" zeigt, wie viele das tatsächlich sind.

Wie finde ich heraus, welche Version eines Pakets mein Server tatsächlich installiert hat?

Prüfen Sie den Inhalt auf der Festplatte, nicht die Lockdatei. npm ls <package> meldet, was in node_modules vorhanden ist, und node -e "console.log(require('./node_modules/<package>/package.json').version)" gibt nur die Versionszeichenfolge aus. Die Lockdatei in git beantwortet eine andere Frage: Was hätte installiert werden sollen? Genau darum geht es beim Vergleich beider Angaben. Wenn Sie Deployments in einem nach dem git-Commit benannten Verzeichnis ablegen, bleiben beide Angaben auch Monate später verfügbar.

Erkennt npm audit Angriffe auf die Software-Lieferkette?

Nein. npm audit vergleicht Ihren Abhängigkeitsbaum mit einer Datenbank gemeldeter Schwachstellen. Daher werden nur Probleme erkannt, die bereits veröffentlicht und mit einer Kennung versehen wurden. Eine bösartige Veröffentlichung bleibt in den Stunden oder Tagen, in denen ihre Installation relevant ist, möglicherweise ungemeldet. npm audit signatures ist der nützlichere Befehl: Er verifiziert Registry-Signaturen für Ihren installierten Abhängigkeitsbaum und prüft Herkunftsatteste, sofern der Herausgeber diese erstellt hat. Dadurch erfahren Sie, ob ein Tarball aus einem öffentlichen Build oder von einer unbekannten Maschine stammt.

Warum ist es wichtig, die Anwendung als nicht privilegierter Benutzer auszuführen, wenn der Angriff bei der Installation erfolgt?

Weil die beiden Fehler unterschiedliche Auswirkungen haben und Sie sich gegen beide schützen müssen. Code zur Installationszeit läuft als Deploy-Benutzer und kann die SSH-Schlüssel, Registry-Token und Cloud-Zugangsdaten dieses Benutzers lesen. Code zur Laufzeit läuft als Dienstkonto. Mit User=nodeapp, ProtectSystem=strict und ohne Zugangsdaten auf der Festplatte bleibt sein Zugriff auf die eigene Anwendungsumgebung und die Datenbank beschränkt. Die Trennung der Konten bedeutet außerdem, dass der Prozess, der den Netzwerkverkehr bedient, node_modules nicht überschreiben kann. Dadurch ist eine Kompromittierung zur Laufzeit beim nächsten Neustart beendet, statt dauerhaft zu werden.