Sicheres Arbeiten mit Node.js und npm: mein Setup gegen Supply-Chain-Angriffe
Wie ich Node.js- und npm-Projekte gegen Supply-Chain-Angriffe härte: Socket Firewall mit Anleitung, .npmrc, gehärtete package.json, npm ci, Provenance-Signaturen — plus ein Copy-paste-Starter.
von Jean Pierre Kolb ·
Diese Seite hier ist eine statische Website. Kein PHP, keine Datenbank, kein Server-Code, der zur Laufzeit ausgeführt wird — am Ende steht nur HTML, CSS und ein bisschen JavaScript. Man könnte meinen, da gäbe es nichts abzusichern. Der Trugschluss steckt im Wort „zur Laufzeit": Der gefährliche Moment ist nicht, wenn ein Besucher die Seite aufruft, sondern wenn ich sie baue. In genau dieser Sekunde lädt npm dutzende, oft hunderte fremde Pakete auf meine Maschine und darf — im Standard — beliebigen Code ausführen. Das ist die Angriffsfläche.
Dieser Artikel ist mein komplettes Setup dagegen, so wie ich es projektübergreifend auf allen jpkc.com-Projekten fahre: Socket Firewall als npm-Wrapper, eine gehärtete .npmrc, eine defensiv konfigurierte package.json, reproduzierbare Installs — und darüber hinaus die Maßnahmen, die über die reine Projekt-Config hinausgehen (Signaturen, Konto-Sicherheit, Dependency-Hygiene). Am Ende gibt es einen Copy-paste-Starter, ein Command-Cheatsheet und eine FAQ. Du kannst das Setup direkt auf eigene Node-Projekte übertragen — egal ob statische Site, CLI-Tool oder App.
Das Bedrohungsmodell in fünf Minuten
Bevor es an die Config geht, lohnt der Blick darauf, wogegen wir uns eigentlich wehren. „Supply-Chain-Angriff" heißt: Nicht deine eigene Software wird angegriffen, sondern etwas, das du vertraust und einbindest — ein Paket, eine Transitive-Dependency, ein Maintainer-Konto. Vier Muster tauchen immer wieder auf:
- Bösartige Install-Hooks. Ein Paket definiert ein
postinstall-Skript, das beimnpm installautomatisch läuft — noch bevor du irgendetwas davon nutzt. Einer der häufigsten Vektoren. Fast alle bekannten Vorfälle haben darüber Daten exfiltriert oder sich eingenistet. - Kompromittierte Versionen. Ein legitimes, beliebtes Paket bekommt eine neue Version mit eingebautem Schadcode — weil das Maintainer-Konto übernommen oder das Projekt an jemanden übergeben wurde.
event-stream(2018) ist der Klassiker: Der neue Maintainer schob eine Dependency nach, die Krypto-Wallets leerräumte. Beiua-parser-js(2021) wurde das Konto gekapert und Versionen mit Krypto-Miner und Passwort-Stealer veröffentlicht. - Typosquatting. Ein bösartiges Paket heißt fast wie ein bekanntes (
crossenvstattcross-env) und wartet auf Vertipper in derpackage.jsonoder imnpm install-Befehl. - Dependency Confusion. Ein Angreifer registriert im öffentlichen Registry einen Namen, den du intern verwendest, mit höherer Versionsnummer — und npm zieht die öffentliche, bösartige Variante statt deiner internen.
Der gemeinsame Nenner: Der Schaden entsteht auf deiner Maschine oder in deiner CI, zur Installations- und Build-Zeit. Genau dort setzt das Setup an.
Baustein 1: Socket Firewall (sfw)
Die wichtigste Regel zuerst, weil sie alle anderen umschließt: Ich rufe npm nie direkt auf — immer über sfw.
Socket Firewall Free ist ein schlanker Wrapper um npm. Er schiebt sich zwischen deinen Befehl und die Registry und analysiert jeden Paket-Fetch in Echtzeit gegen die Bedrohungsdaten von socket.dev. Erkennt er ein bekanntes bösartiges Paket, blockiert er es, bevor es überhaupt auf die Platte geschrieben wird — also bevor ein postinstall-Hook überhaupt eine Chance zum Laufen hätte. Das ist der entscheidende Unterschied zu npm audit: Firewall wirkt proaktiv beim Holen, audit prüft reaktiv den bereits installierten Graphen.
Installation
npm install -g sfw # einmalig, installiert den Wrapper global
sfw --version # verifizieren, z. B. "Socket Firewall Free, version 2.0.6"Kleiner Henne-Ei-Moment, der ehrlicherweise dazugehört: sfw selbst installierst du einmalig mit normalem npm — es ist ja das Werkzeug, das erst danach alle weiteren Installs absichert. Diesen einen Aufruf machst du bewusst und aus vertrauenswürdiger Quelle (dem offiziellen Paket sfw); ab dann läuft alles andere durch die Firewall.
Nutzung
Ab hier bekommt jeder npm-Aufruf das sfw-Präfix:
sfw npm ci # Install aus dem Lockfile
sfw npm run build # Production-Build
sfw npm install <paket>Voraussetzung ist außerdem ein aktuelles Node — ich erzwinge Node.js ≥ 24 (siehe Baustein 3). Prüfen:
node --version # sollte v24.x oder höher sein Baustein 2: .npmrc — zwei Zeilen, die den Unterschied machen
Im Projekt-Root liegt eine .npmrc mit genau zwei Einstellungen. Beide sind kurz, beide sind wirkungsvoll:
# Block install hooks — the most common npm supply-chain vector.
ignore-scripts=true
# Pin exact versions on install; no caret/tilde ranges in package.json.
save-exact=trueignore-scripts=true blockiert preinstall-, postinstall- und prepare-Skripte sämtlicher Pakete. Damit ist einer der häufigsten Angriffsvektoren zu — ein bösartiges Paket kann seinen Code beim Install schlicht nicht mehr automatisch ausführen. Diese eine Zeile fängt die Mehrzahl der real dokumentierten Supply-Chain-Kompromittierungen ab.
save-exact=true sorgt dafür, dass npm install <paket> eine exakte Version in die package.json schreibt — kein ^1.2.3, kein ~1.2.3. Der Effekt: Ein Dependency-Update wird zur bewussten, auditierbaren Handlung statt eines stillen Nebeneffekts. Ohne diese Zeile könnte ein npm install still eine frisch veröffentlichte (womöglich kompromittierte) Patch-Version einziehen.
Der Sonderfall: native Binaries
ignore-scripts=true hat einen Preis. Manche Pakete mit nativen Komponenten — etwa sharp, esbuild, pagefind, lightningcss oder @tailwindcss/cli — brauchten früher ein postinstall, um ihr plattformspezifisches Binary zu holen. Die meisten liefern es heute als fertiges Plattform-Paket über optionalDependencies aus. Der Clou: Selbst Pakete, die weiterhin ein Install-Skript deklarieren — etwa esbuild mit postinstall oder ältere sharp-Versionen mit einem install-Skript — funktionieren unter ignore-scripts=true trotzdem, weil das Binary über die optionale Abhängigkeit kommt und das neutralisierte Skript gar nicht gebraucht wird. Falls nach sfw npm ci aber doch ein Binary fehlt und der Build abbricht, ziehe ich das eine betroffene Paket gezielt und einmalig nach:
sfw npm install --ignore-scripts=false <paket>Wichtig: Das ist eine chirurgische Ausnahme für ein einzelnes, bekanntes Paket — nicht ein pauschales Abschalten von ignore-scripts. Die globale Einstellung bleibt an.
Baustein 3: package.json härten
Drei Felder, die Angriffs- und Fehlerflächen schließen:
{
"name": "my-project",
"private": true,
"type": "module",
"engines": {
"node": ">=24"
},
"devDependencies": {}
}"private": true— schützt vor versehentlichemnpm publish. Ohne dieses Flag würde ein irrtümlichernpm publish-Aufruf im Projektordner deine (womöglich interne) Konfiguration auf die öffentliche npm-Registry hochladen. Ein Tippfehler mit Außenwirkung."engines": { "node": ">=24" }— dokumentiert und erzwingt die Node-Version. Neuere Node-Versionen bringen Sicherheits-Fixes und moderne Defaults; eine Mindestversion verhindert, dass jemand das Projekt auf einem veralteten, verwundbaren Node baut.- Alle Abhängigkeiten unter
devDependencies— für eine statische Site ist der ganze Stack ein Build-Werkzeug. Es gibt keine Runtime-Dependencies, weil zur Laufzeit kein Node läuft. Das sauber zu trennen hält die Angriffsfläche dessen, was im Ernstfall mit ausgeliefert würde, bei null.
Baustein 4: npm ci statt npm install — und Lockfile-Hygiene
Für den Standardfall — „installiere, was das Projekt vorschreibt" — nutze ich nie npm install, sondern:
sfw npm cinpm ci installiert exakt das, was im package-lock.json steht: keine Neu-Auflösung von Versionsbereichen, keine Überraschungen, reproduzierbar und obendrein schneller. npm install dagegen darf den Baum neu auflösen und dabei — je nach Ranges — andere Versionen ziehen als beim letzten Mal. Mit npm ci ist das Lockfile die einzige Wahrheit.
Damit das trägt, gilt: Das package-lock.json gehört committet und wird wie Quellcode behandelt. Und wenn ich den Baum wirklich einmal komplett neu aufbauen will:
rm -rf node_modules package-lock.json && sfw npm installDas ist der einzige Moment, in dem npm install fällt — bewusst, um ein frisches Lockfile zu erzeugen, das ich anschließend prüfe und committe.
Baustein 5: Wartungs-Kadenz
Sicherheit ist kein Einmal-Setup. Einmal pro Woche laufen bei mir zwei Befehle:
sfw npm audit # bekannte CVEs im Dependency-Graph
sfw npm outdated # veraltete Pakete (leere Ausgabe = alles aktuell)npm audit gleicht den installierten Graphen gegen die CVE-Datenbank ab und meldet bekannte Schwachstellen samt Schweregrad. Nicht-invasive Fixes (nur Lockfile) spielt sfw npm audit fix ein; --force würde auch Breaking-Change-Updates ziehen — das nur mit Bedacht und anschließendem Build-Test.
npm outdated zeigt, wo neuere Versionen bereitstehen. Updates mache ich gezielt, nicht pauschal:
sfw npm view <paket> version # aktuelle Version prüfen
sfw npm install <paket>@<neue-version> # gezielt aktualisieren
sfw npm audit # danach prüfen
sfw npm run build # Regressionen ausschließenÜber das Setup hinaus
Die fünf Bausteine oben sind das Fundament. Wer es ernst meint, ergänzt vier Dinge, die über die reine Projekt-Config hinausgehen.
Signaturen und Provenance verifizieren
Seit npm 8.15 lässt sich prüfen, ob die installierten Pakete tatsächlich das sind, was die Registry signiert hat:
sfw npm audit signaturesDer Befehl verifiziert die kryptografischen Registry-Signaturen und — seit npm 9.5, wo vorhanden — Provenance-Attestationen. Provenance (auf Basis von Sigstore) verknüpft ein veröffentlichtes Paket beweisbar mit dem exakten Quell-Commit und dem CI-Build, aus dem es entstanden ist. Ein grünes Ergebnis heißt: Was du installiert hast, stammt nachweislich aus der behaupteten Quelle und wurde unterwegs nicht manipuliert.
Konto-Sicherheit
Die meisten großen Vorfälle begannen mit einem übernommenen Maintainer-Konto, nicht mit einer Code-Lücke. Wer selbst publiziert, schützt sein npm-Konto daher mit 2FA (Zwei-Faktor-Authentifizierung) und nutzt granulare Access-Tokens mit minimalen Rechten und Ablaufdatum statt eines allmächtigen Dauer-Tokens. Ein Token, das nur ein einziges Paket veröffentlichen darf, richtet bei einem Leak kaum Schaden an.
Dependencies minimieren — und vor dem Einbau bewerten
Die sicherste Dependency ist die, die du nicht hast. Jedes Paket bringt seinen eigenen Abhängigkeitsbaum mit; ein harmlos wirkendes Helferlein zieht schnell ein Dutzend Transitive nach. Bevor ein Paket reinkommt, lohnt ein kurzer Blick:
sfw npm view <paket> # Metadaten, Maintainer, letzte Veröffentlichung
sfw npm ls <paket> # zeigt, wer <paket> (transitiv) hereinziehtZusätzlich gibt socket.dev jedem Paket einen Score und markiert auffälliges Verhalten (Netzwerkzugriffe, Shell-Ausführung, Install-Skripte). Kriterien, die ich abklopfe: Wie alt und aktiv ist das Paket? Wie viele Maintainer? Braucht es wirklich Install-Skripte? Steht ein Ein-Zeilen-Nutzen einem großen Transitive-Baum gegenüber?
CI genauso hart wie lokal
Der Build läuft am Ende oft nicht auf meiner Maschine, sondern in der CI — und dort gelten dieselben Regeln: npm ci (nie install), die .npmrc mit ignore-scripts wird auch in der Pipeline respektiert, und Deploy-Tokens bekommen nur die Rechte, die sie brauchen. Eine kompromittierte Dependency in einer CI mit weitreichenden Secrets ist deutlich gefährlicher als lokal.
Welcher Baustein deckt welchen Vektor
| Angriffsvektor | Gegenmaßnahme im Setup |
|---|---|
Bösartiges Install-Skript (postinstall) | ignore-scripts=true |
| Kompromittierte neue Version | save-exact=true + npm ci aus dem Lockfile |
| Typosquatting / bekanntes Malware-Paket | Socket Firewall blockt beim Fetch |
| Versehentliche Veröffentlichung | private: true |
| Unbemerkte transitive CVE | sfw npm audit (wöchentlich) |
| Gefälschtes / manipuliertes Artefakt | sfw npm audit signatures (Provenance) |
| Übernommenes Publish-Konto | npm-2FA + granulare Tokens |
| Aufgeblähte Angriffsfläche | Dependencies minimieren, alles als devDependencies |
Copy-paste-Starter
Für ein neues Projekt reichen diese zwei Dateien als sicheres Fundament.
.npmrc:
# Block install hooks — the most common npm supply-chain vector.
ignore-scripts=true
# Pin exact versions on install; no caret/tilde ranges in package.json.
save-exact=truepackage.json (Auszug — die sicherheitsrelevanten Felder):
{
"name": "my-project",
"private": true,
"type": "module",
"engines": {
"node": ">=24"
},
"devDependencies": {}
}Danach einmalig sfw global installieren (npm install -g sfw), und ab da läuft jeder npm-Aufruf über sfw.
Command-Cheatsheet
| Befehl | Zweck |
|---|---|
sfw npm ci | Reproduzierbarer Install aus dem Lockfile (Standardfall) |
sfw npm install <paket> | Neue Dependency hinzufügen (exakt gepinnt) |
sfw npm install --save-dev <paket> | Explizit als devDependency |
sfw npm install --ignore-scripts=false <paket> | Einmalig ein fehlendes natives Binary nachziehen |
sfw npm audit | Bekannte CVEs im Dependency-Graph prüfen |
sfw npm audit signatures | Registry-Signaturen und Provenance verifizieren |
sfw npm outdated | Veraltete Pakete anzeigen |
sfw npm ls <paket> | Zeigt, wer <paket> als (transitive) Dependency zieht |
sfw npm view <paket> | Metadaten und Versionen eines Pakets |
FAQ
Bremst sfw die Installs spürbar? Nein. Die Fetch-Analyse fällt gegenüber dem eigentlichen Netzwerk-Download kaum ins Gewicht. Der Sicherheitsgewinn steht in keinem Verhältnis zum minimalen Overhead.
Nach sfw npm ci bricht der Build ab, weil ein Binary fehlt — was tun? Das ist der native-Binaries-Sonderfall. Zieh das eine betroffene Paket einmalig mit sfw npm install --ignore-scripts=false <paket> nach. ignore-scripts bleibt global an.
Brauche ich das wirklich für eine rein statische Website? Ja. Der Angriff passiert zur Build-Zeit auf deiner Maschine oder in der CI, nicht zur Laufzeit der ausgelieferten Seite. Ob am Ende HTML oder eine App herauskommt, ändert nichts an der Install-Angriffsfläche.
Ist sfw ein Ersatz für npm audit? Nein, die beiden ergänzen sich. sfw blockt proaktiv beim Fetch (bekannte Malware, bevor sie auf die Platte kommt), npm audit prüft reaktiv den installierten Graphen gegen CVE-Datenbanken. Beides zusammen deckt Prävention und Erkennung ab.
Kostet Socket Firewall etwas? Socket Firewall Free ist kostenlos. Für Teams gibt es darüber hinaus kostenpflichtige Socket-Angebote, aber die hier beschriebene Absicherung läuft vollständig mit der Free-Variante.
Weiterlesen
- Claude Code im Container — Isolation auf einer weiteren Ebene: Build- und Agenten-Workflows in Podman/Docker kapseln.
- Claude Code konfigurieren — Permissions, Hooks und Subagents sicher einrichten.
- Cheat Sheets — kompakte Kommando-Referenzen, u. a. zu Node, npm und Kommandozeile.
- Socket Firewall Free und socket.dev — die Werkzeuge hinter diesem Setup.