# 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.

Source: https://www.jpkc.com/db/blog/nodejs-npm-absichern/

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](https://github.com/SocketDev/sfw-free) 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 beim `npm install` automatisch 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`](https://snyk.io/blog/a-post-mortem-of-the-malicious-event-stream-backdoor/) (2018) ist der Klassiker: Der neue Maintainer schob eine Dependency nach, die Krypto-Wallets leerräumte. Bei [`ua-parser-js`](https://de.wikipedia.org/wiki/Supply-Chain-Attacke) (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 (`crossenv` statt `cross-env`) und wartet auf Vertipper in der `package.json` oder im `npm 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](https://github.com/SocketDev/sfw-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](https://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

```bash
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:

```bash
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:

```bash
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:

```ini
# 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=true
```

**`ignore-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`](https://sharp.pixelplumbing.com/), `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:

```bash
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:

```json
{
  "name": "my-project",
  "private": true,
  "type": "module",
  "engines": {
    "node": ">=24"
  },
  "devDependencies": {}
}
```

- **`"private": true`** — schützt vor versehentlichem `npm publish`. Ohne dieses Flag würde ein irrtümlicher `npm 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:

```bash
sfw npm ci
```

`npm 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:

```bash
rm -rf node_modules package-lock.json && sfw npm install
```

Das 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:

```bash
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:

```bash
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:

```bash
sfw npm audit signatures
```

Der Befehl verifiziert die kryptografischen Registry-Signaturen und — seit npm 9.5, wo vorhanden — **Provenance-Attestationen**. Provenance (auf Basis von [Sigstore](https://www.sigstore.dev/)) 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:

```bash
sfw npm view <paket>        # Metadaten, Maintainer, letzte Veröffentlichung
sfw npm ls <paket>          # zeigt, wer <paket> (transitiv) hereinzieht
```

Zusätzlich gibt [socket.dev](https://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`:

```ini
# 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=true
```

`package.json` (Auszug — die sicherheitsrelevanten Felder):

```json
{
  "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](https://www.jpkc.com/db/blog/claude-code-container/) — Isolation auf einer weiteren Ebene: Build- und Agenten-Workflows in Podman/Docker kapseln.
- [Claude Code konfigurieren](https://www.jpkc.com/db/blog/claude-code-konfiguration/) — Permissions, Hooks und Subagents sicher einrichten.
- [Cheat Sheets](https://www.jpkc.com/db/cheatsheets/) — kompakte Kommando-Referenzen, u. a. zu Node, npm und Kommandozeile.
- [Socket Firewall Free](https://github.com/SocketDev/sfw-free) und [socket.dev](https://socket.dev/) — die Werkzeuge hinter diesem Setup.

