mitmario.dev

Abhängigkeiten prüfen

Node.js Sandbox 4 Min Lesezeit 3 BeispieleLektion 6 von 8

In 5.5 ging es um die Frage, ob ein Paket ins Projekt gehört. Hier geht es um die Frage danach: Was ist mit den Paketen, die schon drin sind?

Was npm audit kann

npm audit vergleicht die Versionen in deinem Lockfile gegen eine Datenbank gemeldeter Sicherheitslücken und sagt dir, welche davon betroffen sind. Das ist nützlich, kostet nichts und gehört in jeden Projektalltag.

Was npm audit findet und was nicht
// So arbeitet npm audit: Es vergleicht deine Versionen gegen eine
// Datenbank gemeldeter Lücken. Was dort nicht steht, sieht es nicht.
const datenbank = [
  { paket: "minimatch", unter: "3.0.5", schwere: "high", titel: "ReDoS im Muster-Abgleich" },
  { paket: "tar", unter: "6.1.9", schwere: "critical", titel: "Pfade brechen aus dem Zielordner aus" },
];

const installiert = [
  { paket: "minimatch", version: "3.0.4" },
  { paket: "tar", version: "6.2.1" },
  { paket: "buntes-log", version: "2.0.1", postinstall: "curl https://… | sh" },
];

function kleinerAls(a, b) {
  const [a1, a2, a3] = a.split(".").map(Number);
  const [b1, b2, b3] = b.split(".").map(Number);
  if (a1 !== b1) return a1 < b1;
  if (a2 !== b2) return a2 < b2;
  return a3 < b3;
}

function pruefe(paket, version) {
  const eintrag = datenbank.find((e) => e.paket === paket);
  if (!eintrag) return "kein Eintrag in der Datenbank";
  return kleinerAls(version, eintrag.unter) ? `${eintrag.schwere}: ${eintrag.titel}` : "Version ist neu genug";
}

for (const { paket, version, postinstall } of installiert) {
  console.log(`${paket.padEnd(12)} ${version.padEnd(8)} ${pruefe(paket, version)}`);
  if (postinstall) console.log(`${"".padEnd(12)} ${"".padEnd(8)} postinstall: ${postinstall}`);
}

console.log("");
console.log("buntes-log führt beim Installieren fremden Code aus und steht in");
console.log("keiner Datenbank. npm audit meldet dazu nichts, weil es nichts weiß.");

Und es ist genau so weit nützlich, wie die Datenbank reicht. Der erste der drei Fälle ist ein Treffer: eine gemeldete Lücke, eine betroffene Version, ein klarer Hinweis. Der zweite ist dasselbe Paket in einer neueren Version, alles gut. Der dritte ist der interessante: Das Paket steht in keiner Datenbank und führt beim Installieren einen Befehl aus, der etwas aus dem Netz holt und ausführt.

npm audit sagt dazu nichts, und zwar nicht aus Nachlässigkeit, sondern weil es nichts wissen kann. Eine Lücke muss erst jemandem auffallen, gemeldet, geprüft und veröffentlicht werden, und das dauert. Frisch untergeschobener Schadcode ist in dieser Zeit unsichtbar.

Merksatz: Ein grünes npm audit heißt „nichts Gemeldetes”, nicht „nichts Schlimmes”.

Warum npm ci auf dem Server steht und nicht npm install

Der Bereich, den das Lockfile schließt
// Was die Registry für dieses Paket kannte, damals und heute.
const imJanuar = ["4.18.1", "4.18.2"];
const heute = ["4.18.1", "4.18.2", "4.19.0", "4.21.2", "5.0.0", "5.2.1"];

// "^4.18.2" heißt: alles ab 4.18.2, solange die erste Zahl gleich bleibt.
function loeseAuf(bereich, registry) {
  const ziffern = bereich.replace("^", "");
  const [haupt] = ziffern.split(".").map(Number);
  return registry.filter((v) => Number(v.split(".")[0]) === haupt).at(-1);
}

console.log('In der package.json steht:  "express": "^4.18.2"');
console.log("");
console.log(`npm install  im Januar      ${loeseAuf("^4.18.2", imJanuar)}`);
console.log(`npm install  heute          ${loeseAuf("^4.18.2", heute)}`);
console.log("");
console.log('Im Lockfile steht:          "version": "4.18.2"');
console.log('                            "integrity": "sha512-oxlOK…"');
console.log("");
console.log("npm ci       im Januar      4.18.2");
console.log("npm ci       heute          4.18.2");
console.log("");
console.log("Der Bereich sagt, was erlaubt ist. Das Lockfile sagt, was war.");
console.log("Und die Prüfsumme sagt, dass es dasselbe Paket ist wie damals.");

In deiner package.json steht ein Bereich, etwa ^4.18.2. Der sagt: alles ab dieser Version, solange die erste Zahl gleich bleibt. npm install löst diesen Bereich auf, und zwar gegen die Registry von jetzt. Im Januar kam dabei 4.18.2 heraus, heute 4.21.2, und niemand hat etwas geändert.

Das Lockfile hält fest, was tatsächlich installiert wurde, und zwar nicht nur die Version, sondern auch eine Prüfsumme je Paket. npm ci liest ausschließlich das Lockfile, installiert genau diese Versionen und rechnet die Prüfsummen nach. Damit ist dein Server nicht davon abhängig, was in der Registry seit gestern passiert ist.

Genau hier haben die Prüfsummen ihren eigentlichen Zweck: Sie sind nicht dazu da, Übertragungsfehler zu finden, sondern dazu, dass ein Paket unter derselben Versionsnummer nicht heimlich einen anderen Inhalt bekommen kann. Deshalb gehört das Lockfile in die Versionskontrolle, und deshalb heißt der Befehl auf dem Server npm ci.

Der Angriff über die Lieferkette

Das Muster ist immer dasselbe, und dein Paket ist dabei nicht das Ziel. Das Ziel ist eines, von dem deins abhängt, und zwar möglichst weit unten: ein kleines Hilfspaket, das seit Jahren niemand pflegt und das zufällig in ein paar Millionen Projekten steckt.

Der Weg dorthin ist meistens kein technischer Einbruch, sondern ein sozialer. Jemand bietet der überlasteten Autorin an, die Wartung zu übernehmen, arbeitet ein paar Monate ordentlich mit und veröffentlicht dann eine Version, die beim Installieren etwas Zusätzliches tut. In anderen Fällen reicht ein Konto ohne Zwei-Faktor-Anmeldung.

Wie viel fremder Code beim Installieren läuft
// Ein kleiner Abhängigkeitsbaum. Drei der vier Pakete bringen Skripte
// mit, die beim Installieren laufen, und drei davon gehören dir nicht.
const baum = [
  { name: "meine-app", eigenes: true, skripte: ["prepare"] },
  { name: "buntes-log", eigenes: false, skripte: ["postinstall"] },
  { name: "schnell-parser", eigenes: false, skripte: ["install", "postinstall"] },
  { name: "farben", eigenes: false, skripte: [] },
];

function laufen(mitSkripten) {
  return baum.flatMap((p) => (mitSkripten ? p.skripte.map((s) => `${p.name}:${s}`) : []));
}

const alle = laufen(true);
const fremde = alle.filter((eintrag) => !eintrag.startsWith("meine-app:"));

console.log(`npm install                    ${alle.length} Skripte laufen`);
for (const eintrag of alle) console.log(`                               ${eintrag}`);
console.log("");
console.log(`npm install --ignore-scripts   ${laufen(false).length} Skripte laufen`);
console.log("");
console.log(`Davon aus fremdem Code:        ${fremde.length}`);
console.log("Sie laufen unter deinem Benutzer und dürfen alles, was du darfst.");

Das Beispiel zeigt, warum der Moment der Installation so beliebt ist. In einem gewöhnlichen Baum laufen dabei Skripte, und die meisten davon gehören dir nicht. Sie laufen unter deinem Benutzer, mit deinen Rechten, in deinem Projektverzeichnis, und wenn das eine CI-Umgebung ist, dann mit deinen Zugangsdaten darin. npm install --ignore-scripts schaltet sie ab, und für die allermeisten Projekte fehlt danach nichts.

Genau diese Vorsichtsmaßnahme steckt übrigens im Installationsbefehl der Sandbox, in der dieser Kurs läuft: Sie installiert mit --ignore-scripts. Zum Einsatz kommt der Befehl hier nie, denn die beiden Pakete des Kurses liegen fertig auf der Maschine und werden nur kopiert. Aber der Weg, den es nicht mehr braucht, soll trotzdem der sichere sein.

Die zwei Maßnahmen, die im Alltag am meisten bringen

Weniger Pakete. Jede Abhängigkeit ist ein Stück fremder Code mit eigenen Abhängigkeiten und eigenen Wartungsproblemen. Die Frage aus 5.5 gilt weiter: Wie viele Zeilen wären es von Hand?

Updates bewusst statt automatisch. Ein Bot, der jeden Tag alles aktualisiert und selbst merged, ist genau der Weg, auf dem eine übernommene Version am schnellsten bei dir landet. Besser: in Abständen, gebündelt, mit einem Blick auf das Changelog und darauf, wer das Paket veröffentlicht hat.

Dazu kommt eine dritte, die nichts kostet: Prüf im Bau, nicht von Hand. Ein Programm, das den Bericht liest und mit Exit-Code 1 endet, wenn etwas Kritisches darin steht, hält die Pipeline an, bevor die Lücke im Betrieb landet. Genau das baust du in der Challenge.

Eine ausführlichere Fassung des Themas mit echten Fällen steht im Blog unter „Poisoned Repos”.

Zum Mitnehmen

npm audit findet, was gemeldet ist. Frisch untergeschobener Schadcode ist nirgends gemeldet, und genau deshalb ist eine grüne Prüfung kein Freibrief.

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.

Was in dieser Lektion steckt

  • Artikel mit 3 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe, dein Code läuft auf einem Server

    Öffnet sich mit dem Basis Konto.