Veraltete npm-Abhängigkeiten in einem fünf Jahre alten Frontend: Wo man anfängt

#veraltete npm abhängigkeiten

Gründer & Lead Developer

Experte für Softwareentwicklung und Legacy-Code-Optimierung

LinkedIn

Wer npm outdated in einem Frontend ausführt, das seit fünf Jahren nicht mehr angefasst wurde, sieht im Terminal mehr roten Text, als man erwartet hat. Hundert Pakete, vielleicht zweihundert. Die Hälfte davon liegt drei Hauptversionen zurück. Der erste Impuls ist, das Terminal zu schließen und ein Ticket für ein "vollständiges Dependency-Upgrade" anzulegen – das dann ein weiteres Jahr im Backlog liegen bleibt, weil niemand ein Projekt ohne klaren Endpunkt übernehmen möchte.

Dieser Impuls ist verständlich, behandelt aber jedes veraltete Paket als dasselbe Problem. Das ist es nicht. Veraltete npm-Abhängigkeiten in einer Jahre alten React-Anwendung fallen typischerweise in drei unterschiedliche Kategorien, die weder das gleiche Risiko noch die gleichen Kosten zur Behebung mit sich bringen. Wer sie in der falschen Reihenfolge abarbeitet, macht aus einer zweiwöchigen Aufräumaktion schnell eine zweimonatige.

Veraltete npm-Abhängigkeiten in drei Kategorien einteilen

Führt man npm audit zusammen mit npm outdated aus, erhält man zwei überlappende, aber unterschiedliche Bilder. npm outdated zeigt, wie weit man zurückliegt. npm audit zeigt, welche dieser Lücken gerade wirklich gefährlich sind. Beide Listen müssen gemeinsam gelesen werden, denn ein Paket kann zehn Versionen zurückliegen und keine bekannte Schwachstelle haben – oder nur eine Version zurückliegen und eine kritische Lücke aufweisen.

Bevor irgendetwas angefasst wird, sollte die komplette Liste in drei Kategorien sortiert werden:

Sicherheitspatches sind Pakete mit einer veröffentlichten CVE gegen die eingesetzte Version, für die bereits eine behobene Version existiert. Das sind meist kleine, gezielte Releases – ein Patch-Versionssprung, kein Major-Release –, weil Maintainer einen Sicherheitsfix normalerweise nicht mit brechenden API-Änderungen bündeln. Lodash, minimist und axios haben in den letzten Jahren genau diese Art von Patch-Release ausgeliefert: gleiche API, eine geschlossene Schwachstelle.

Build-Tooling umfasst Webpack, Babel, ESLint, den eigenen Bundler, den Test-Runner und alles andere, was den Code berührt, ohne selbst an den Browser ausgeliefert zu werden. Diese Kategorie hinkt in einem alten Projekt meist am weitesten hinterher, weil niemand eine veraltete Dev-Dependency so bemerkt wie ein kaputtes Feature. Sie ist in der Regel auch die sicherste Kategorie für Updates, denn ein fehlschlagendes Build-Tool zeigt sich sofort als roter CI-Lauf – nicht als Bug, den ein Kunde drei Wochen später meldet.

Das Framework und seine direkten Laufzeit-Abhängigkeiten sind alles, was die Anwendung tatsächlich importiert und ausliefert: React, der Router, die State-Management-Bibliothek, das UI-Komponenten-Kit. Diese Kategorie trägt pro Update das größte Risiko, aber nach Abschluss auch den größten Nutzen, denn eine veraltete Framework-Version ist es, die irgendwann den Einsatz einer Bibliothek, eines Hosting-Features oder die Einstellung eines Entwicklers verhindert, der nur mit der aktuellen Version des Ökosystems arbeiten möchte.

Jede Kategorie schlägt auch unterschiedlich fehl, wenn ein Update danebengeht. Ein fehlerhafter Sicherheitspatch ist nahezu ausgeschlossen, da Maintainer in einem Patch-Release keine brechenden Änderungen ausliefern. Ein fehlgeschlagenes Build-Tool-Update lässt die CI-Pipeline fehlschlagen – das sieht man vor dem Merge. Ein fehlgeschlagenes Framework-Update kann jeden bestehenden Test bestehen und trotzdem eine subtil fehlerhafte Seite erzeugen, die ein Nutzer früher findet als die eigene Testsuite, weil die Verhaltensänderung von Anfang an nicht durch einen Test abgedeckt war.

Warum Sicherheitspatches zuerst kommen – und warum das keine Ermessensfrage ist

Von den drei Kategorien ist nur eine mit einer Uhr verbunden: die Sicherheitspatches. OWASP listet veraltete und verwundbare Komponenten seit Jahren unter den Top-Risiken für Webanwendungen, und der Mechanismus ist fast immer derselbe: Eine CVE wird veröffentlicht, der Fix ist öffentlich, und ab diesem Zeitpunkt ist die Lücke zwischen der eigenen Version und der gepatchten Version für jeden sichtbar, der das Dependency-Manifest oder die HTTP-Response-Header prüft. Man muss nicht raten, ob ein Angreifer das finden könnte. Der Fix wird zusammen mit dem Problem veröffentlicht.

Die gute Nachricht: Diese Kategorie geht meist schnell. Zuerst npm audit fix ausführen. Der Befehl erledigt die Patch-Level-Sprünge automatisch und fasst nichts an, was einen Major-Versionssprung erfordert. Was danach übrig bleibt, ist die kleinere, schwierigere Menge: Schwachstellen, deren Fix ein brechendes Upgrade erfordert – dazu mehr im Abschnitt zum Framework weiter unten. Aber der automatische Durchlauf allein räumt typischerweise den größten Teil der Liste an einem einzigen Nachmittag ab, ohne Codeänderungen jenseits der Lockfile.

Build-Tooling als zweiter Schritt, weil es laut statt leise ausfällt

Sobald die Sicherheitspatches erledigt sind, folgt das Build-Tooling – nicht, weil es wichtiger wäre als das Framework, sondern weil Fehler hier günstiger sind. Ein Webpack-Major-Upgrade, das etwas kaputt macht, lässt den Build fehlschlagen. Man erfährt es in der CI, bevor irgendetwas ausgeliefert wird, und der Fix ist meist eine Konfigurationsänderung, die im Migrationsleitfaden des Tools selbst dokumentiert ist.

Hier liegen auch meist die ältesten, am stärksten vernachlässigten Pakete. Ein seit fünf Jahren nicht gewartetes Frontend läuft oft noch auf Webpack 4 oder einer ESLint-Konfiguration von vor der Einführung der Flat Config. Diese Tools werden ignoriert, weil sie unsichtbar sind, solange sie funktionieren. Niemand eröffnet ein Ticket für "unser Bundler funktioniert noch einwandfrei", selbst wenn er Jahre veraltet ist, weil nichts an der Nutzererfahrung direkt davon abhängt.

Ein Tool nach dem anderen aktualisieren, nicht alle auf einmal. Bundler aktualisieren, kompletten Build- und Testlauf durchführen, committen. Linter-Konfiguration aktualisieren, laufen lassen, committen. Wer das als ein einziges großes Upgrade behandelt, macht aus einer Ein-Tages-Aufgabe eine Woche voller Entwirrung, welche von sechs gleichzeitigen Änderungen den Build kaputt gemacht hat.

Das Framework und seine Laufzeit-Abhängigkeiten, zuletzt und mit der größten Sorgfalt

Das ist die Kategorie, in der die Fünf-Jahres-Lücke tatsächlich schmerzt, und in die der meiste verbleibende Aufwand fließt. Wer drei React-Hauptversionen zurückliegt, führt nicht ein Upgrade durch, sondern drei in Folge, jedes mit eigenen veralteten APIs, jedes in einem separaten Migrationsleitfaden dokumentiert, jedes mit eigener manueller Testrunde, weil die automatisierte Suite für das alte Verhalten geschrieben wurde und gegen ein subtil kaputtes neues Verhalten bereitwillig grün anzeigt.

Das React-Team dokumentiert, wie die meisten Framework-Maintainer, brechende Änderungen Release für Release statt Versionssprung für Versionssprung. Es lohnt sich also, jeden Major-Version-Migrationsleitfaden in der richtigen Reihenfolge durchzugehen, statt nach einer einzigen Anleitung zu suchen, die drei Versionen gleichzeitig abdeckt. Zu erwarten sind veraltete Lifecycle-Methoden, entfernte APIs, die der eigene Code noch aufruft, und Peer-Dependency-Konflikte, bei denen die eigene State-Management-Bibliothek oder das UI-Kit die angepeilte React-Version noch nicht unterstützt. Das Changelog jeder wichtigen Abhängigkeit auf Framework-Kompatibilität prüfen, bevor man den Sprung beginnt – nicht erst, wenn der Konflikt auftritt.

Wenn das Frontend auf etwas noch Älterem als React selbst aufbaut – jQuery-Ära-Code, der noch in einem modernen Build lebt –, sieht der Upgrade-Pfad wieder anders aus. Diese spezielle Situation, die schrittweise statt in einem einzigen Rewrite migriert wird, behandeln wir in unserem Leitfaden zur schrittweisen Frontend-Modernisierung von jQuery zu React. Die Priorisierungslogik ist dieselbe wie hier beschrieben. Der Unterschied ist der Umfang, nicht der Ansatz.

Eine Checkliste für die erste Woche

Zuerst mit npm audit fix die automatischen Sicherheitspatches abarbeiten, danach prüfen, was npm audit anschließend noch anzeigt, denn das Verbleibende braucht einen manuellen Major-Version-Sprung statt eines automatischen. Als Nächstes npm outdated für die devDependencies prüfen, also Build-Tools und Linter, und diese einzeln aktualisieren, mit vollständigem Testlauf zwischen jeder Änderung. Erst danach zum Framework und seinen direkten Laufzeit-Abhängigkeiten übergehen, Version für Version durch jeden Migrationsleitfaden, statt den gesamten Sprung auf einmal zu versuchen. Nebenbei alles löschen, was der Code nicht mehr importiert. Eine fünf Jahre alte package.json enthält meist Pakete, die seit Jahren niemand mehr aufgerufen hat – wer sie vor dem Update entfernt, verschwendet keine Zeit mit der Reparatur eines Upgrades für Code, der gar nicht mehr läuft.

Keiner dieser Schritte erfordert spezielles Tooling über das hinaus, was npm bereits mitbringt. Was sie erfordern, ist diese Reihenfolge – damit ein Fehler in einer Kategorie nicht mit einem Fehler in einer anderen verwechselt wird, und damit die riskanteste Arbeit zuletzt passiert, wenn der Code bereits stabil genug ist, sie zu verkraften.

Wenn die Lücke zu groß ist, um sie nebenbei zu schließen

Manche Codebasen sind lange genug vernachlässigt worden, dass daraus mehr als eine Woche konzentrierter Arbeit wird: Dutzende Pakete mit ineinander verschachtelten Peer-Dependency-Konflikten, eine Framework-Version, die so alt ist, dass die aktuelle Dokumentation sie kaum noch erwähnt, und eine Testsuite, die zu dünn ist, um das abzufangen, was ein Major-Versionssprung kaputt machen könnte. An diesem Punkt ist die Frage nicht mehr, welches Paket als Nächstes aktualisiert wird, sondern ob der Code erst einmal strukturell betrachtet werden muss, bevor irgendetwas davon sicher versucht werden kann.

Genau dafür ist ein Code-Qualitäts-Audit gedacht: eine ehrliche Einschätzung, wie viel vom Risiko aus der Abhängigkeitsliste stammt und wie viel aus dem Code darunter. Deckt das Audit echte strukturelle Altlasten auf, ist eine Legacy-Code-Optimierung der nächste Schritt – genau die Art von gestuftem, testabgedecktem Upgrade, die oben beschrieben wurde.

Wenn die eigene npm outdated-Liste den Punkt erreicht hat, an dem es sicherer wirkt, sie zu ignorieren als anzufassen, ist das ein Gespräch wert – statt eines weiteren Jahres Aufschub. Einfach schreiben an hello@wolf-tech.io oder vorbeischauen auf wolf-tech.io. Die meisten dieser Aufräumarbeiten dauern kürzer, als der Backlog vermuten lässt.