npm-Lieferkettenangriffe auf dem eigenen Server verhindern
So erreichen npm-Lieferkettenangriffe Ihre Node-App auf dem VPS: schädliche Patch-Releases, postinstall-Skripte, Typosquatting und die sichere Deployment-Praxis.
Was ein npm-Lieferkettenangriff auf Ihrem Server ist
Ein npm-Lieferkettenangriff erreicht Ihren Server über ein Paket, das Sie selbst zur Installation ausgewählt haben. Dabei ist kein offener Port und kein Exploit-Schritt erforderlich. npm (node package manager) installiert Code, und bei der Installation von Code wird dieser ausgeführt. Eine kleine Node-Anwendung zieht daher mehrere hundert Pakete nach, die Sie nie gelesen haben. Jedes davon kann eine Stunde später eine neue Version veröffentlichen.
Ihr Deployment ruft eine schädliche 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, weil es eine interne Registry, ein Review-Team und einen Spiegel der öffentlichen Registry hat. Sie haben ein Deployment-Script.
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 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 derselbe Git-Commit und derselbe Deployment-Vorgang am selben Nachmittag zwei verschiedene Codesätze installieren. Diese Lücke ist die Angriffsfläche. Dafür muss nichts auf Ihrem Rechner kompromittiert sein.
Bösartige Releases werden normalerweise gemeldet und zurückgezogen. Der Rückzug erfolgt 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 Bereiche bei jedem Lauf neu auflöst, gelangt automatisch in dieses Zeitfenster. Das geschieht mehrmals pro Woche, ohne dass jemand dies bewusst entscheidet.
Form 2: Ein Installationsskript läuft mit den Rechten des Benutzers, der das Deployment ausführt
Das package.json eines Pakets kann in seinem Block scripts die Einträge preinstall, install, postinstall und prepare deklarieren. npm führt sie während der Installation aus. Sie sind nicht sandboxed, und niemand überprüft sie. Es handelt sich um Shell-Befehle, die mit den Rechten des Benutzers ausgeführt werden, der den Installationsbefehl eingegeben hat. Sie laufen 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 Deployment-Host gehören dazu ~/.npmrc mit einem Registry-Token, ~/.ssh/id_ed25519, das als Deploy-Key für SSH (Secure Shell) verwendet wird, ~/.aws/credentials, ~/.docker/config.json und jede exportierte Variable der Shell. Dort befindet sich normalerweise auch DATABASE_URL.
Ein Payload wie dieser benötigt weder Persistenz noch eine Rechteausweitung. Er liest einige Dateien, sendet sie über HTTPS an einen Host und wird anschließend mit dem Status 0 beendet. Sie sehen nichts, weil npm die Ausgabe von Installationsskripten standardmäßig ausblendet. Deaktivieren Sie dieses Verhalten und beobachten Sie, was tatsächlich ausgeführt wird:
npm ci --foreground-scriptsforeground-scripts verwendet die Standardeingabe sowie die Standardausgabe und die Standardfehlerausgabe des npm-Prozesses. 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 richtig eingegeben haben
Ein Typosquat ist ein Paket, das unter einem Namen veröffentlicht wird, der einem bekannten Namen ähnelt. Es wartet auf einen falsch eingegebenen oder falsch eingefügten Installationsbefehl. Der Mechanismus liegt im Befehl, nicht im Code. Eine Lockfile hilft daher hier nicht: Sie fügen den falschen Namen einmal hinzu, und danach pinnt die Lockfile 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 jeder ein solches Paket veröffentlichen. npm löst nicht mit einem Scope versehene Namen gegen die standardmäßige öffentliche Registry auf. Dadurch kann das öffentliche Paket verwendet werden. Die Lösung ist ein Scope, den Sie selbst kontrollieren, plus eine 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 berücksichtigt wird. Ein internes Paket ohne Scope hat keine solche Zuordnung und ist daher nicht geschützt.
Bevor Sie eine neue Abhängigkeit hinzufügen, prüfen Sie das Paket selbst und nicht nur die Download-Anzeige:
npm view some-lib repository.url maintainers time.created time.modifiedEin Paket, das letzten Monat erstellt wurde und von einem Konto veröffentlicht wird, das Sie keinem öffentlichen Repository zuordnen können, stellt ein anderes Risiko dar als ein Paket mit einer sechsjährigen Historie. Keine dieser Tatsachen ist ein Beweis. Beide lassen sich jedoch mit geringem Aufwand prüfen.
Form 4: Die Person, der sich der Besitzer einer Abhängigkeit unbemerkt geändert hat
Maintainer übergeben Pakete. Jemand ist ausgebrannt, eine unbekannte Person bietet Hilfe an, die Veröffentlichungsrechte wechseln, und keine Benachrichtigung erreicht die Projekte, die davon abhängen. Nichts wurde kompromittiert. Das Vertrauen, das Sie 2021 gewährt haben, liegt jetzt bei einer anderen Person.
Dies ist die langsamste Form und am schwersten zu erkennen. Kein Befehl beantwortet diese Frage direkt. Zwei Maßnahmen schränken das Risiko ein. Prüfen Sie mit der oben gezeigten npm view-Zeile, wer ein Paket veröffentlichen darf, bevor Sie es übernehmen. 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.3Die 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, sollte vollständig gelesen werden, bevor es Ihren Server erreicht.
Mit einer festgeschriebenen Lockdatei und npm ci installieren
package-lock.json dokumentiert für jedes Paket im Abhängigkeitsbaum die exakte Version, die URL der Quelle, einen sha512-Integritätshash des Tarballs und das Paket, das die Abhängigkeit benötigt. 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 Rechnern, die keine Entwickler-Laptops sind:
npm ci --omit=dev --ignore-scriptsnpm ci unterscheidet sich in mehreren für diesen Fall wichtigen Punkten von npm install. Eine Lockdatei muss vorhanden sein. Vor dem Start wird ein vorhandenes node_modules entfernt. Überbleibsel aus einem früheren Deployment können daher nicht in dieses Deployment gelangen. npm ci schreibt weder in package.json noch in die Lockdatei. Eine Installation kann Sie daher nicht unbemerkt auf eine neuere Version bringen. Stimmen Lockdatei und package.json nicht überein, wird der Vorgang mit einem Fehler beendet, anstatt die Abweichung aufzulösen.
Dieser Fehler ist eine Funktion und keine Störung. Eine Änderung an einer Abhängigkeit muss dadurch als Commit eingehen, den jemand geprüft hat. Sie kann nicht als Nebeneffekt eines Deployments um 02:00 eingespielt werden.
Der Integritätshash wird bei jedem Abruf geprüft. Stimmen die Bytes eines Tarballs nicht mit dem gespeicherten Hash überein, schlägt die Installation mit code EINTEGRITY fehl, anstatt den Tarball zu entpacken. Diese Prüfung bietet eine konkrete Garantie: Sie bestätigt, dass die empfangene Datei der von der Lockdatei festgelegten Datei entspricht. Das ist dieselbe Garantie wie beim Prüfen von Downloads mit Checksums. Sie ist jedoch in gleicher Weise eingeschränkt. Die Prüfung sagt nichts darüber aus, ob die festgelegte Version bereits bei ihrer Veröffentlichung manipuliert war.
Ein Detail zu --omit=dev: Diese Pakete werden weiterhin aufgelöst und in die Lockdatei geschrieben. Sie werden lediglich nicht auf dem Datenträger abgelegt. Weniger Pakete auf dem Datenträger bedeuten weniger Installationsskripte und weniger Code, der zur Laufzeit geladen wird. Daher ist diese Option sinnvoll. Sie entfernt eine Abhängigkeit jedoch nicht aus Ihrem Abhängigkeitsbaum.
Installationsskripte sind Code: Sie müssen wissen, wann Sie sie ablehnen
Sie können Installationsskripte deaktivieren. Tragen Sie dies in .npmrc des Projekts ein und committen Sie die Datei neben der Lockdatei:
ignore-scripts=true
save-exact=trueignore-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 ^1.4.2 verwendet. Dadurch gelangt ein auflösbarer Bereich nicht versehentlich in Ihr Manifest.
Dadurch können Funktionen ausfallen. Sie sollten daher wissen, wie Sie damit umgehen, bevor Sie die Option 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. Verwenden Sie in diesem Fall eine Allowlist:
npm ci --ignore-scripts
npm rebuild better-sqlite3npm rebuild <package> führt die Build-Skripte dieses einen Pakets aus. Sie treffen damit eine Entscheidung für jedes Paket, statt einigen hundert unbekannten Paketen pauschal die Ausführungsberechtigung zu erteilen.
Um zu sehen, wie umfangreich diese Berechtigung derzeit 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 Prozess für den 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. Zur Laufzeit ausgeführter Code kann dies ebenfalls tun.
Trennen Sie beide. Erstellen Sie die Anwendung als ein Benutzer, und lassen Sie sie als ein anderer Benutzer bedienen. Das Verzeichnis mit dem bereitgestellten Code muss für den Serving-Account schreibgeschützt sein:
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/nodeappLassen Sie systemd diese Trennung 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.targetProtectSystem=strict bindet das gesamte Dateisystem für diesen Dienst schreibgeschützt ein, ausgenommen /dev, /proc, /sys und alle Pfade, die Sie in ReadWritePaths angeben. Ein Schreibversuch der Anwendung in node_modules schlägt daher mit EROFS: read-only file system fehl. Sie können dies in Ihren eigenen Logs innerhalb von etwa einer Minute nachvollziehen und 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 Listen zur systemd-Härtung. Sie verhindert jedoch den Start von Node, weil V8 JavaScript zur Laufzeit in Maschinencode kompiliert und Speicherbereiche benötigt, die gleichzeitig beschreibbar und ausführbar sind. Übernehmen Sie zweitens den Pfad ExecStart aus command -v node. Wenn Node mit einem Versionsmanager installiert wurde, liegt es im Home-Verzeichnis des Deploy-Benutzers. ProtectHome=yes blendet dieses Verzeichnis anschließend für den Dienst aus. Die Unit schlägt dann sofort mit status=203/EXEC fehl, und ein Log-Eintrag meldet, dass die ausführbare Datei nicht gefunden werden konnte.
Prüfen Sie das Ergebnis, statt der Unit-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/probesystemd-analyze security listet jede Einstellung zur Härtung 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 Dateibesitzrechte falsch. Dann überdecken die systemd-Einstellungen das Problem stillschweigend.
Ein Hinweis zu EnvironmentFile: systemd liest diese Datei als root ein, bevor der Prozess zu User=nodeapp wechselt. Die Datei kann daher mit den Rechten 600 durch root:root geschützt werden. Die Anwendung erhält die Variablen trotzdem. Jeder Benutzer mit einer Shell als nodeapp kann sie weiterhin aus /proc/<pid>/environ lesen. Dadurch wird das Secret im Ruhezustand geschützt, nicht jedoch der laufende Prozess.
Build-Zugangsdaten aus der Build-Umgebung heraushalten
Installationsskripte übernehmen die Umgebung. Diese Tatsache sollte bestimmen, wo Sie den Build ausführen.
Am sichersten ist es, den Build nicht auf dem Produktionsserver auszuführen und anschließend das fertige Verzeichnis dorthin zu kopieren. Die Build-Maschine enthält dann nur ein Registry-Token mit Leserechten. Sie enthält keinen SSH-Deploy-Key, keinen Cloud-Access-Key, kein Datenbankpasswort und keine Anmeldung für eine Container-Registry.
npm token create --read-onlyEin Token mit Leserechten kann Pakete abrufen, aber nichts veröffentlichen. Wird es aus der Build-Umgebung gestohlen, besteht der mögliche Schaden darin, dass öffentliche Pakete heruntergeladen werden können.
Wenn Sie den Build auf dem Server ausführen müssen, führen Sie ihn als Benutzer deploy mit einer bewusst eingeschränkten Umgebung aus. Bewahren Sie die Laufzeitgeheimnisse in /etc/nodeapp/env auf, auf die deploy nicht zugreifen kann. Dasselbe gilt für Build-Automatisierung, die Sie selbst betreiben: Ein selbst gehosteter GitHub-Actions-Runner enthält Token und führt bei jedem Job beliebigen veröffentlichten Code aus. Damit ist er die wertvollste Maschine in einer kleinen Deployment-Umgebung. Jedes Programm, das Sie nicht selbst geschrieben haben und das Ihre gesamte 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.
Pinnen oder paketieren, was Sie nicht prüfen können
Eine angeheftete Abhängigkeit ist eine Abhängigkeit, deren Version sich ohne einen Commit nicht ändern kann. Die versionierte Lockdatei stellt dies bereits für den gesamten Abhängigkeitsbaum sicher. 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. overrides in package.json erzwingt eine Version an jeder Stelle im Abhängigkeitsbaum:
{
"overrides": {
"some-transitive-lib": "1.4.2"
}
}Führen Sie npm install nach dem Hinzufügen einmal 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. Paketieren Sie es selbst. 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 sich nicht unbemerkt ändern. Sie übernehmen damit jedoch dauerhaft auch dessen 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 Wartefrist, die nichts kostet:
npm install --before=2026-08-01Die 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 Veröffentlichung aktiv und noch nicht gemeldet ist. Das Verfahren ist grob, weil dadurch auch echte Sicherheitskorrekturen zurückgehalten werden. Verwenden Sie es, um die Versionsbereiche aufzulösen, prüfen Sie die Änderungen und committen Sie anschließend die Lockdatei.
Woran erkenne ich, welche Version tatsächlich ausgeliefert wurde?
Die Lockfile in git gibt an, was installiert werden sollte. Die Festplatte 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 tatsächlich vorhanden ist, statt was die Lockfile vorgibt. Die Zeile node -e liest das installierte Manifest anhand seines Pfads. Das funktioniert auch bei Paketen, deren Feld exports Importe von Unterpfaden verhindert. Außerdem wird genau eine Version ausgegeben, ohne darum einen Baum zu zeichnen.
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 Informationen dauerhaft her, indem Sie den Commit in das Deployment-Layout aufnehmen. Veröffentlichen Sie die Version in /srv/nodeapp/releases/<short commit sha> und verweisen Sie mit einem Symlink 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 Personen verfügbar, die nicht für das Deployment zuständig waren.
Prüfen Sie abschließend, wofür die Registry bürgt:
npm audit signaturesDamit werden die Registry-Signaturen der Pakete im installierten Baum sowie die Provenienz-Attestierungen für Pakete geprüft, die solche Attestierungen besitzen. Die Provenienz verknüpft ein veröffentlichtes Tarball mit dem öffentlichen Continuous-Integration-(CI-)Build, durch den es erstellt wurde. Eine verifizierte Attestierung ermöglicht daher, den Code bis zu einem Commit zurückzuverfolgen 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 es lief.
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. Rotieren Sie das Registry-Token, die SSH-Schlüssel in dessen Home-Verzeichnis, die Cloud-Zugangsdaten und alle Secrets, die in dieser Shell exportiert waren. Rotation ist die einzig angemessene Reaktion, weil Sie nicht beweisen 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 und alles, was über deren Netzwerkzugriff erreichbar ist. Genau darin liegt der Grund für die Ausführung von Diensten als nicht privilegierte Benutzer auf einem VPS. Dadurch wird der Einbruch nicht verhindert. Es bestimmt, auf welchen Teil des Systems sich der Einbruch auswirkt und ob er einen Neustart übersteht.
Erstellen Sie das System anschließend neu, statt es zu bereinigen. Löschen Sie node_modules, setzen Sie das betroffene Paket in package.json auf eine Version unterhalb der fehlerhaften Version fest, führen Sie npm install einmal aus, um die Lock-Datei zu aktualisieren, committen Sie die Änderung und stellen Sie mit npm ci bereit. Reparieren Sie keinen bestehenden Verzeichnisbaum direkt. Sie können nicht vollständig ermitteln, welche Dateien ein Installationsskript verändert hat.
Dokumentieren Sie außerdem das Zeitfenster: das erste Deployment, das die Version hätte einbinden können, und das Deployment, das sie entfernt hat. Anhand dieses Bereichs können Sie feststellen, welche eigenen Logs Sie prüfen müssen. Das ist nur möglich, wenn Ihre Releases nach Commits benannt sind.
Was dadurch nicht behoben wird
Eine Lockdatei macht eine Abhängigkeit nicht sicher. Sie macht aus dem Zeitpunkt, an dem Sie diese Abhängigkeit akzeptiert haben, eine datierte und geprüfte Entscheidung statt einer unbeabsichtigten Folge eines Deployments. Jede der oben beschriebenen Maßnahmen führt dieselbe Umwandlung durch: aus einem Zufall wird eine bewusste Entscheidung.
npm audit ist hier keine Abwehrmaßnahme. Das Tool vergleicht Ihren Abhängigkeitsbaum mit einer Datenbank gemeldeter Sicherheitslücken. Es findet daher Probleme, die bereits veröffentlicht und benannt wurden. Ein Angriff auf die Lieferkette bleibt während seiner gesamten wirksamen Lebensdauer unbenannt. Führen Sie npm audit für alte, bekannte Fehler aus. Erwarten Sie davon keine Erkenntnisse zu einem Release, das vor vier Stunden veröffentlicht wurde.
Weniger Abhängigkeiten zu verwenden hilft mehr als jedes Tool in diesem Leitfaden. Trotzdem ist dies der unbeliebteste 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 niemals als Ihr Deploy-Benutzer ausgeführt.
Das alles gilt nicht nur für npm. Dieselben vier Muster gelten für PyPI, RubyGems, Container-Images und den Paketmanager Ihrer Distribution. Bei npm tritt das Problem besonders deutlich auf, weil die Abhängigkeitsbäume tiefer sind und Installationsskripte standardmäßig ausgeführt werden. Wie viel von der umgebenden Maschine Sie absichern müssen, hängt davon ab, wo der Code ausgeführt wird. Das ist Teil der weitergehenden Frage, ob VPS-Hosting sicher ist.
FAQ
Schützt npm ci mich 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 aufzeichnet, prüft jedes Tarball gegen seinen sha512-Integritäts-Hash und beendet sich mit einem Fehler, wenn package.json und die Lock-Datei nicht übereinstimmen, statt die Abweichung aufzulösen. Ob die festgelegte Version sicher ist, sagt das nicht aus. Wenn Sie eine Lock-Datei committen, die eine schädliche Version festlegt, installiert npm ci diese Version bei jedem Durchlauf auf jedem Ihrer Server zuverlässig.
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, dass Installationsskripte von Abhängigkeiten ausgeführt werden. 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 ein vorkompiliertes Binary abrufen, benötigen ihre Skripte tatsächlich. Wenn die Skripte deaktiviert sind, schlagen diese Pakete später zur Laufzeit mit 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 der Festplatte, nicht die Lock-Datei. 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 Lock-Datei in git beantwortet eine andere Frage: Welche Version hätte installiert werden sollen? Genau deshalb werden die beiden Angaben verglichen. Wenn Sie Deployments in ein nach dem git-Commit benanntes Verzeichnis ausführen, bleiben beide Angaben auch Monate später verfügbar, wenn Sie sie benötigen.
Erkennt npm audit Angriffe auf die 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 schädliche 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 überprüft die Registry-Signaturen des gesamten installierten Baums und prüft Herkunftsbescheinigungen, sofern der Herausgeber solche erstellt hat. Dadurch lässt sich feststellen, ob ein Tarball aus einem öffentlichen Build oder von einer unbekannten Maschine stammt.
Warum ist es wichtig, die Anwendung als unprivilegierter Benutzer auszuführen, wenn der Angriff bei der Installation erfolgt?
Weil die beiden Fehler unterschiedliche Reichweiten haben und Sie sich gegen beide schützen. Code zur Installationszeit läuft als Deploy-Benutzer und kann die SSH-Schlüssel, Registry-Tokens und Cloud-Zugangsdaten dieses Benutzers lesen. Code zur Laufzeit läuft als Service-Account. Mit User=nodeapp, ProtectSystem=strict und ohne Zugangsdaten auf der Festplatte beschränkt sich seine Reichweite auf die eigene Umgebung der Anwendung und deren Datenbank. Die Trennung der Konten bedeutet außerdem, dass der Prozess, der den Netzwerkverkehr bedient, node_modules nicht überschreiben kann. Eine Kompromittierung zur Laufzeit ist dadurch beim nächsten Neustart beendet, statt dauerhaft zu werden.