Wichtigste Erkenntnisse
- Eine Versionserhöhung ist kein Patch, und Aktualisierungen von Abhängigkeiten können möglicherweise inkompatible Änderungen mit sich bringen.
- Erzwungene Upgrades können bereits bei der Kompilierung zu offensichtlichen Fehlern führen oder subtilere Regressionen zur Laufzeit verursachen, die bei Tests übersehen werden.
- Transitive Sicherheitslücken erschweren die Behebung, da sich das betroffene Paket möglicherweise mehrere Ebenen unterhalb Ihrer direkten Abhängigkeiten befindet.
- Wenn ein Upgrade zum jetzigen Zeitpunkt nicht sicher ist, sind Ausgleichsmaßnahmen und Patches vor Ort nach wie vor realistische Optionen.
„Einfach aktualisieren.“ Oder die ähnliche Variante: „Einfach die Abhängigkeit aktualisieren.“
Wenn Sie im Bereich der Softwaresicherheit tätig sind, haben Sie diese Aussage wahrscheinlich schon unzählige Male in der einen oder anderen Form gehört. Eine Sicherheitslücke taucht auf, eine neuere Version enthält den Fix, und die Lösung scheint auf der Hand zu liegen: Einfach die Version aktualisieren und weitermachen.
Produktionssoftware funktioniert selten so. Ein Upgrade ist oft die richtige langfristige Lösung, und die Aktualisierung von Abhängigkeiten reduziert technische Schulden, verschafft Teams Zugang zu Sicherheitspatches aus dem Upstream und erleichtert zukünftige Migrationen. Aber jede Schwachstelle als Aufforderung zu einem sofortigen Upgrade der Abhängigkeiten zu betrachten, ignoriert die komplexe Realität der Wartung von Software, auf die sich Menschen tatsächlich verlassen. Eine Produktionsanwendung ist eine getestete Kombination aus APIs, Laufzeitumgebungen, Bibliotheken, Konfigurationen, Build-Tools, Integrationen und Annahmen, die zufällig gerade zusammen funktionieren. Ändert man ein Teil, muss man oft mehrere ändern.
Damit stehen Entwickler vor der Herausforderung, zwei berechtigte Risiken gegeneinander abzuwägen: eine bekannte Sicherheitslücke offen zu lassen oder beim Versuch, diese zu beheben, einen Ausfall im Produktivbetrieb zu verursachen. Der Ratschlag „Einfach ein Update durchführen“ tut so, als gäbe es nur das erste Risiko.
Die Falle der Breaking Changes
Kompatibilitätsänderungen stellen eines der größten Risiken bei übereilten Upgrades von Abhängigkeiten dar, insbesondere wenn Sicherheitskorrekturen eine Hauptversionsgrenze überschreiten.
Nehmen wir zum Beispiel „npm audit“. Es identifiziert Schwachstellen im gesamten Abhängigkeitsbaum und empfiehlt Updates, mit denen diese behoben werden können. In vielen Fällen, npm audit fix wendet kompatible Korrekturen automatisch an. Hinzufügen --forceAllerdings installiert npm dann auch Pakete außerhalb der von Ihnen festgelegten Abhängigkeitsbereiche, einschließlich SemVer-Major-Updates Ihrer obersten Abhängigkeiten. npm beschreibt das Flag als Möglichkeit, Schutzmechanismen zu deaktivieren, die normalerweise unerwünschte Nebenwirkungen verhindern, und warnt davor, es zu verwenden, es sei denn, Sie wissen genau, was es bewirkt.
Diese Unterscheidung ist wichtig. Ein Sicherheitshinweis kann in einer transitiven Abhängigkeit wie „rollup“ auftauchen – also in einer Komponente, die über Ihr Build-Tooling bereitgestellt wurde und nicht von Ihnen selbst ausgewählt wurde. Die vorgeschlagene Abhilfe könnte dann etwa so lauten: „Es wird @angular-devkit/build-angular@X installiert, was eine kompatibilitätsbrechende Änderung darstellt.“ Und aus der einzeiligen Abhängigkeitsaktualisierung, mit der Sie begonnen haben, ist unbemerkt eine Migration des Build-Toolings geworden.
Angular ist ein gutes Beispiel dafür, warum diese Abkürzung schädlich ist. Größere Angular-Upgrades sollen ng update, das die entsprechenden Update- und Migrationsschemata ausführt, und Angular erfordert, dass Upgrades über mehrere Hauptversionen hinweg jeweils nur eine Hauptversion nach der anderen erfolgen. Die spezielle Update-Anleitung behandelt zudem die Code-Umwandlungen, Konfigurationsänderungen, Abhängigkeits-Updates und manuellen Arbeiten, die ein Upgrade mit sich bringen kann. Das erzwungene Hochstufen von Paketversionen mit npm audit fix --force umgeht diesen Migrationsworkflow und kann dazu führen, dass Sie sich mit den kompatibilitätsbrechenden Änderungen auseinandersetzen müssen, ohne über die entsprechenden Migrationen zu verfügen.
Die Forschungsergebnisse weisen in dieselbe Richtung. In seinem „Dependency Management Report“ stellte Endor Labs fest, dass 95 % der analysierten Versions-Upgrades mindestens eine potenziell kompatibilitätsbeeinträchtigende Änderung enthielten und dass 24 % der Schwachstellen in Abhängigkeiten ein Update auf eine neue Hauptversion erforderten. Selbst Updates auf Patch-Ebene wiesen eine Wahrscheinlichkeit von 75 % auf, eine Änderung zu enthalten, die möglicherweise zu einem Ausfall eines Clients führen könnte. Diese Zahlen stammen aus dem Datensatz von Endor und nicht aus den Gesetzen der Physik, doch die Art des Problems wird jedem bekannt vorkommen, der diese Situation schon einmal erlebt hat.
Das bedeutet jedoch nicht, dass Sie auf ein Upgrade verzichten sollten. Es bedeutet vielmehr, dass ein Sicherheitspatch und ein umfangreiches Anwendungs-Upgrade nicht automatisch dasselbe sind. Wenn man beides kombiniert, nur weil ein Scanner einen roten Alarm ausgelöst hat, wird aus einer gezielten Behebungsmaßnahme ein Modernisierungsprojekt im Eilverfahren.
API- und Konfigurationsfehler
Jede ausgereifte Anwendung stützt sich auf ein Geflecht aus Vereinbarungen, von denen einige offensichtlich, andere hingegen kaum dokumentiert sind. Die Weitergabe von Zuständen zwischen Komponenten stützt sich auf eine bestimmte Bibliotheks-API. Das Bündeln, Kompilieren, Transpilieren oder Minifizieren von Assets stützt sich auf Build-Tools und deren Konfigurationsdateien. Authentifizierung, Datenbankzugriff, Serialisierung, Beobachtbarkeit, Caching, Messaging: mehr Abhängigkeiten, mehr öffentliche APIs, mehr Annahmen.
Ein umfangreiches Upgrade der Abhängigkeiten kann diese Annahmen auf eine Weise beeinträchtigen, die aus Sicht des Upstream-Projekts völlig legitim, aus Ihrer Sicht jedoch äußerst unangenehm ist. Eine exportierte Methode wird umbenannt. Eine Funktion akzeptiert nun andere Argumente. Eine veraltete API verschwindet endgültig. Ein Standardwert ändert sich. Eine Konfigurationsoption wird nicht mehr erkannt. Ein Plugin erwartet eine neuere Peer-Abhängigkeit. Die Node-Version, auf der Ihre Anwendung noch läuft, liegt außerhalb des unterstützten Bereichs der neuen Version.
Nun weist Ihr Sicherheitsupdate Kompilierungsfehler, fehlgeschlagene Builds, fehlerhafte Tests und ein Laufzeitverhalten auf, das untersucht werden muss. Manchmal übernehmen automatisierte Migrationswerkzeuge den Großteil davon, und manchmal muss die Anwendung ziemlich grundlegend überarbeitet werden.
So oder so verdient diese Arbeit dieselbe technische Sorgfalt wie jede andere Produktionsänderung. Sie sollte nicht einfach so durchgewunken werden, nur weil sie mit einer CVE begann.
Risiken einer Rückentwicklung
Dann gibt es noch die Fehler, die sich nicht durch einen auffälligen roten Build ankündigen. Ein Upgrade einer Abhängigkeit kann problemlos kompiliert werden, die offensichtlichen Tests bestehen und dennoch das Verhalten an einer Stelle verändern, auf die man nicht geachtet hat. Ein Parser verschärft die Validierung. Ein HTTP-Client ändert ein Standard-Timeout. Eine Bibliothek bleibt technisch API-kompatibel, verhält sich aber unter einer bestimmten Auslastung anders. Die Testsuite bleibt grün, weil niemand jemals eine Assertion für das alte Verhalten geschrieben hat.
An dieser Stelle wird die Sicherheit von Abhängigkeiten eher zu einem betrieblichen Problem als zu einem Problem der Paketverwaltung. Bei einem Hobbyprojekt sind die Auswirkungen lästig. Bei einer Bankplattform, einem Zahlungssystem, einer Anwendung im Gesundheitswesen oder einer Fertigungsumgebung geht es nicht nur darum, ob man auf die korrigierte Version umsteigen kann, sondern auch darum, was sich dadurch sonst noch ändert.
Testumgebungen, Integrationstests, Änderungsfenster, Canary-Bereitstellungen und Rollback-Verfahren gibt es genau aus diesem Grund. Die Sicherheit mindert die Bedeutung dieser Kontrollmaßnahmen nicht. Die Dringlichkeit macht sie sogar noch wichtiger.
Operative Reibungsverluste
An dieser Stelle beginnt in der Regel die berühmte Debatte zwischen Sicherheit und Entwicklung. Die Sicherheitsabteilung entdeckt eine Schwachstelle mit hohem Schweregrad und möchte, dass diese unverzüglich behoben wird. Die Entwicklungsabteilung sieht, dass ein Upgrade einer Abhängigkeit Dutzende von Komponenten betrifft, und benötigt Zeit, um dies zu testen. Der Betrieb sieht ein Produktionssystem mit einer vorgeschriebenen Verfügbarkeitsanforderung und möchte den Nachweis, dass die Änderung den Betrieb nicht zum Erliegen bringt. Jeder reagiert rational auf eine andere Art von Risiko.
Das Problem tritt auf, wenn das Unternehmen die Diskussion auf zwei Optionen reduziert: sofortiges Upgrade oder Akzeptanz der Sicherheitslücke. Das Abhängigkeitsmanagement in der Praxis benötigt mehr Optionen als das. Sicherheitsteams benötigen eine zuverlässige Möglichkeit, das Risiko zu minimieren. Entwickler benötigen Zeit, um die Auswirkungen auf die Kompatibilität zu verstehen. Betriebsteams benötigen vorhersehbare Änderungen. Produktteams haben nach wie vor Roadmaps und Verpflichtungen gegenüber Kunden, die nicht jedes Mal verschwinden, wenn eine neue CVE bekannt wird.
Die Ausnahme schriftlich festzuhalten ist in der Regel besser, als ein weiteres Treffen darüber abzuhalten. Nennen Sie die Ausgleichsmaßnahme, benennen Sie den Verantwortlichen und legen Sie ein Ablaufdatum fest. Dadurch wird aus einer Pattsituation eine dokumentierte Entscheidung, für die jemand verantwortlich ist – was eine echte Praxis des Sicherheitsmanagements darstellt und keine Verzögerungstaktik ist.
Die versteckte Offenlegung in transitiven Abhängigkeiten
Transitive Abhängigkeiten erschweren all dies zusätzlich. Sie haben diese Pakete nicht selbst ausgewählt. Sie sind hinzugekommen, weil etwas, das Sie bewusst installiert haben, von ihnen abhängt, das wiederum von etwas anderem abhängt, und schon bald hat eine überschaubare Anzahl direkter Abhängigkeiten einen viel größeren Graphen hervorgebracht. Die Studie „State of Dependency Management“ von Endor Labs ergab, dass etwa 95 % der Schwachstellen in ihrem Datensatz in transitiven Komponenten lagen und nicht in den Paketen, die Entwickler bewusst ausgewählt hatten.
Lockfiles dienen unter anderem dazu, diesen aufgelösten Baum reproduzierbar zu machen. npm’s package-lock.json Es wird der genaue Abhängigkeitsbaum gespeichert, sodass spätere Installationen diesen reproduzieren, anstatt stillschweigend eine andere Kombination von Versionen aufzulösen, und die Abhängigkeitssperre von Gradle funktioniert im Wesentlichen genauso, indem sie die aufgelösten Modulversionen dauerhaft speichert. Diese Stabilität ist nützlich, bedeutet aber auch, dass anfällige indirekte Pakete mehrere Ebenen unterhalb dessen liegen können, womit sich ein Entwickler tagtäglich beschäftigt.
Die Behebung eines Problems ist selten so einfach wie ein manueller Versionswechsel. Das anfällige Paket unterliegt möglicherweise Einschränkungen durch die direkte Abhängigkeit, über die es eingebunden wurde; ein Upgrade dieses übergeordneten Pakets kann zur Aktualisierung mehrerer anderer transitiver Pakete führen, und jedes dieser Pakete bringt seine eigenen Kompatibilitätsaspekte mit sich. Beginnen Sie mit npm ls <package> oder npm explain <package> um nachzuverfolgen, über welchen Abhängigkeitspfad die anfällige Version in den Baum gelangt ist. Hat der übergeordnete Zweig eine korrigierte Version veröffentlicht, ist ein Upgrade die sauberste Lösung. Ist dies nicht der Fall, dann ein overrides Ein solcher Eintrag kann die gepatchte Version während der Wartezeit tiefer in den Baum verschieben, aber betrachten Sie dies eher als eine Kompatibilitätsänderung, die getestet werden muss, und nicht als kostenlosen Patch. Überschreibungen verändern die Art und Weise, wie npm eine Version auflöst. Sie stellen jedoch nicht sicher, dass die übergeordnete Version damit kompatibel ist.
Die eigentliche Änderung verläuft also selten geradlinig vom anfälligen Paket zum korrigierten Paket. Vielmehr sieht der Ablauf so aus: Upgrade der übergeordneten Komponente, neuer Abhängigkeitsgraphen, Kompatibilitätstests, Änderungen an der Anwendung. Das ist eine ganz andere technische Entscheidung, und genau deshalb sollten Abhängigkeits-Upgrades als Änderungen am Abhängigkeitsgraphen und nicht als Bearbeitungen der Versionsnummern geprüft werden.
Ein ehrlicherer Rahmen
Ein Notfall ist ein schrecklicher Moment, um festzustellen, dass Ihre Strategie zum Schwachstellenmanagement vollständig davon abhängt, dass Modernisierungsmaßnahmen bei Bedarf durchgeführt werden.
Die Modernisierung gehört unbedingt zur Lebenszyklusplanung. Teams sollten wissen, welche Frameworks, Laufzeitumgebungen und Bibliotheken veralten, und sie sollten regelmäßig Updates durchführen, anstatt zuzulassen, dass sich über Jahre hinweg technische Schulden ansammeln. Kleinere, routinemäßige Upgrades sind viel leichter zu bewältigen als riesige Migrationen, die unter Zeitdruck durchgeführt werden. Es gibt jedoch einen wichtigen Unterschied zwischen „Wir sollten von dieser Version wegkommen“ und „Wir müssen noch heute Abend von dieser Version wegkommen, um eine Sicherheitslücke zu schließen.“ Das sind nicht dieselben Aussagen.
Manchmal ist die sicherste Lösung tatsächlich ein sofortiges Upgrade. Wenn es ein unkompliziertes, kompatibles Update gibt, die Tests überzeugend sind und die Auswirkungen auf den Betrieb gering sind, sollten Sie es durchführen. Manchmal ist der anfällige Code in der tatsächlichen Bereitstellung Ihrer Anwendung nicht erreichbar und kann entsprechend priorisiert werden. Manchmal verringern Ausgleichsmaßnahmen das Risiko, während das Upgrade ordnungsgemäß getestet wird. Und manchmal ist ein Sicherheitspatch vor Ort die bessere kurzfristige Lösung, da er die Sicherheitslücke beseitigt, ohne die umfassenderen Verhaltensänderungen eines Haupt-Releases zu erzwingen. Das Ziel ist nicht, eine Modernisierung zu vermeiden. Es geht darum, den Sicherheitstermin vom Migrationstermin zu trennen, wenn die Umstände dies erfordern.
Dies wird besonders wichtig, sobald eine Abhängigkeit das Ende ihres Lebenszyklus erreicht hat, da der Support durch den Hersteller dann eingestellt wird und Teams vor der Wahl stehen, entweder bekannte Sicherheitslücken in Kauf zu nehmen oder hastig auf eine Version umzusteigen, für deren Ausführung ihre Anwendung nie ausgelegt war. Der „Endless Lifecycle Support“ (ELS) von TuxCare ist genau für diese Situation konzipiert. Er bietet fortlaufende CVE-Patches für abgedeckte Betriebssysteme, Laufzeitumgebungen, Bibliotheken und Anwendungen, deren Lebenszyklus abgelaufen ist, einschließlich zurückportierter Korrekturen, die es Unternehmen ermöglichen, die Versionen weiter zu nutzen, von denen ihre Software abhängt, während eine längerfristige Migration geplant wird. Das macht ein Upgrade nicht überflüssig. Es gibt den Teams das, was ihnen normalerweise fehlt: Zeit für ein ordnungsgemäßes Upgrade.
SecureChain geht das Problem bereits in einer früheren Phase des Lebenszyklus an. Anstatt sich blind auf in öffentlichen Registern veröffentlichte Pakete zu verlassen, werden die betroffenen Komponenten aus verifizierten Quellcodes neu kompiliert, auf bösartige Muster und Risiken in der Lieferkette überprüft und kontinuierlich überwacht sowie bei Auftreten von Schwachstellen gepatcht – begleitet von SBOM-, VEX- und SLSA-Level-3-Herkunftsnachweisen. Wenn die Wartung durch den Upstream-Anbieter endet, bietet ELS den Patching-Pfad nach dem Ende des Lebenszyklus (EOL) für unterstützte Komponenten.
Der Grundgedanke ist ganz einfach: Sicherheitsmaßnahmen sollten Entwickler nicht dazu zwingen, ihre Anwendungen jedes Mal unnötig neu zu schreiben, wenn der Lebenszyklus der Software eines anderen ausläuft.
In unserem Leitfaden zu Alternativen zu Endor Labs können Sie erfahren, wie andere Supply-Chain-Tools im Vergleich abschneiden.
Abschließende Überlegungen
„Einfach aktualisieren“ klingt gut, weil es ein heikles technisches Problem auf drei Wörter reduziert. Dem Abhängigkeitsdiagramm ist das egal.
Upgrades sind nach wie vor wichtig, und ein angemessenes Maß an Aktualität ist eine der besten Möglichkeiten, um zukünftige Upgrades überschaubar zu halten. Machen Sie sich mit den Lebenszyklen der Software vertraut, auf die Sie sich verlassen, behalten Sie Abhängigkeitsstrukturen im Blick, testen Sie Upgrades kontinuierlich und vermeiden Sie es, dass sich über Jahre hinweg unbemerkt technische Schulden anhäufen. Wenn jedoch eine Sicherheitslücke auftritt, lautet die richtige Frage nicht automatisch, wie schnell Sie auf die neueste Version umsteigen können. Die Frage ist vielmehr, wie diese Sicherheitslücke am sichersten behoben werden kann, ohne dabei ein anderes Problem in der Produktion zu verursachen. Manchmal fallen diese Antworten identisch aus. Manchmal jedoch nicht.
Beginnen Sie mit Ihrem eigenen Repository. Öffnen Sie die Lock-Datei, sehen Sie sich die ältesten Abhängigkeiten im Baum an und finden Sie heraus, wer sie noch pflegt, welche Versionen tatsächlich unterstützt werden, was davon abhängt und was ein Upgrade tatsächlich betreffen würde. Tun Sie dies, bevor ein Notfall eintritt, denn eine Abhängigkeitsmigration, die Sie verstehen, ist Ingenieursarbeit, während eine, die Sie während eines Sicherheitsvorfalls improvisieren, ein Glücksspiel ist.
