Seit Jahrzehnten wird die Wiederverwendung von Software häufiger diskutiert als sie tatsächlich war. Heute ist die Situation umgekehrt: Entwickler verwenden die Programme anderer Leute jeden Tag in Form von Software-Abhängigkeiten wieder, und das Problem selbst bleibt fast unerforscht.
Meine eigene Erfahrung umfasst ein Jahrzehnt der Arbeit mit
dem internen Repository von
Google , in dem Abhängigkeiten als vorrangiges Konzept festgelegt werden, sowie die Entwicklung
eines Abhängigkeitssystems für die Programmiersprache Go .
Abhängigkeiten bergen ernsthafte Risiken, die zu oft übersehen werden. Der Übergang zur einfachen Wiederverwendung kleinster Softwareteile erfolgte so schnell, dass wir noch keine Best Practices für die effektive Auswahl und Verwendung von Abhängigkeiten entwickelt haben. Auch um Entscheidungen zu treffen, wann sie angemessen sind und wann nicht. Der Zweck dieses Artikels ist es, Risiken zu bewerten und die Suche nach Lösungen in diesem Bereich anzuregen.
Was ist Sucht?
In der modernen Entwicklung ist
Abhängigkeit zusätzlicher Code, der von einem Programm aufgerufen wird. Durch das Hinzufügen einer Abhängigkeit wird die Wiederholung bereits erledigter Arbeiten vermieden: Entwerfen, Schreiben, Testen, Debuggen und Unterstützen einer bestimmten Codeeinheit. Wir nennen diese Codeeinheit ein
Paket , obwohl auf einigen Systemen andere Begriffe wie eine Bibliothek oder ein Modul anstelle eines Pakets verwendet werden.
Das Akzeptieren externer Abhängigkeiten ist eine alte Praxis: Die meisten Programmierer haben die erforderliche Bibliothek heruntergeladen und installiert, sei es PCRE oder zlib von C, Boost oder Qt von C ++, JodaTime oder Junit von Java. Diese Pakete verfügen über hochwertigen Debug-Code, dessen Erstellung viel Erfahrung erfordert. Wenn ein Programm die Funktionalität eines solchen Pakets benötigt, ist es viel einfacher, das Paket manuell herunterzuladen, zu installieren und zu aktualisieren, als diese Funktionalität von Grund auf neu zu entwickeln. Aufgrund der hohen Vorabkosten ist die manuelle Wiederverwendung jedoch teuer: Winzige Pakete lassen sich leichter selbst schreiben.
Ein Abhängigkeitsmanager (manchmal auch als Paketmanager bezeichnet) automatisiert das Herunterladen und Installieren von Abhängigkeitspaketen. Da Abhängigkeitsmanager das Herunterladen und Installieren einzelner Pakete vereinfachen, können kleine Pakete durch die Senkung der Fixkosten kostengünstig veröffentlicht und wiederverwendet werden.
Beispielsweise bietet ein Node.js-Abhängigkeitsmanager namens NPM Zugriff auf über 750.000 Pakete. Eine davon,
escape-string-regexp
, enthält eine einzelne Funktion, die Operatoren für reguläre Ausdrücke aus Eingabedaten entgeht. Alle Implementierung:
var matchOperatorsRe = /[|\\{}()[\]^$+*?.]/g; module.exports = function (str) { if (typeof str !== 'string') { throw new TypeError('Expected a string'); } return str.replace(matchOperatorsRe, '\\$&'); };
Bevor die Abhängigkeitsmanager auftauchten, war es unmöglich, sich vorzustellen, eine Bibliothek mit acht Zeilen zu veröffentlichen: zu viel Overhead und zu wenig Nutzen. NPM reduzierte den Overhead jedoch auf nahezu Null, so dass nahezu triviale Funktionen gepackt und wiederverwendet werden konnten. Ende Januar 2019 wurde die
escape-string-regexp
Abhängigkeit in fast tausend andere NPM-Pakete integriert, ganz zu schweigen von allen Paketen, die Entwickler für ihren eigenen Gebrauch schreiben und nicht öffentlich veröffentlichen.
Jetzt sind Abhängigkeitsmanager für fast jede Programmiersprache erschienen. Maven Central (Java), Nuget (.NET), Packagist (PHP), PyPI (Python) und RubyGems (Ruby) - jedes von ihnen verfügt über mehr als 100.000 Pakete. Das Aufkommen einer derart weit verbreiteten Wiederverwendung kleiner Pakete ist eine der größten Veränderungen in der Softwareentwicklung in den letzten zwei Jahrzehnten. Und wenn wir nicht vorsichtiger sind, wird dies zu ernsthaften Problemen führen.
Was könnte schief gehen?
Im Rahmen dieser Diskussion wird ein Paket aus dem Internet heruntergeladen. Durch Hinzufügen einer Abhängigkeit wird die Arbeit zur Entwicklung dieses Codes - Entwerfen, Schreiben, Testen, Debuggen und Support - einer anderen Person im Internet übertragen, die Sie normalerweise nicht kennen. Mit diesem Code setzen Sie Ihr eigenes Programm den Auswirkungen aller Abstürze und Mängel der Abhängigkeit aus. Die Ausführung Ihrer Software
hängt jetzt buchstäblich
vom Code eines Fremden aus dem Internet ab. Um es so auszudrücken, alles klingt sehr unsicher. Warum sollte jemand dem überhaupt zustimmen?
Wir sind uns einig, weil es einfach ist, weil alles zu funktionieren scheint, weil es auch alle anderen tun und vor allem, weil es eine natürliche Fortsetzung einer jahrhundertealten etablierten Praxis zu sein scheint. Aber es gibt einen wichtigen Unterschied, den wir ignorieren.
Vor Jahrzehnten vertrauten die meisten Entwickler auch anderen, Programme zu schreiben, von denen sie abhängig waren, wie z. B. Betriebssysteme und Compiler. Diese Software wurde aus bekannten Quellen gekauft, oft mit einer Art Support-Vereinbarung. Es gibt immer noch Raum für
Fehler oder völlige Zerstörung . Aber wir wussten zumindest, mit wem wir es zu tun hatten, und konnten in der Regel kommerzielle oder rechtliche Einflussmaßnahmen anwenden.
Das Phänomen der Open-Source-Software, die kostenlos über das Internet verbreitet wird, hat die alte Praxis des Kaufs von Software weitgehend verdrängt. Als die Wiederverwendung noch schwierig war, führten nur wenige Projekte solche Abhängigkeiten ein. Obwohl ihre Lizenzen in der Regel auf „Garantien von kommerziellem Wert und Eignung für einen bestimmten Zweck“ verzichteten, bauten die Projekte einen guten Ruf auf. Benutzer haben diesen Ruf bei ihren Entscheidungen weitgehend berücksichtigt. Anstelle von kommerziellen und rechtlichen Eingriffen kam die Reputationsunterstützung. Viele gängige Pakete dieser Zeit genießen immer noch einen guten Ruf: zum Beispiel BLAS (veröffentlicht 1979), Netlib (1987), libjpeg (1991), LAPACK (1992), HP STL (1994) und zlib (1995).
Stapelmanager haben das Modell der Wiederverwendung von Code auf extreme Einfachheit reduziert: Jetzt können Entwickler den Code präzise für einzelne Funktionen in Dutzenden von Zeilen freigeben. Dies ist eine großartige technische Leistung. Es stehen unzählige Pakete zur Verfügung, und ein Projekt kann eine große Anzahl davon enthalten, aber kommerzielle, rechtliche oder Reputationscode-Vertrauensmechanismen gehören der Vergangenheit an. Wir vertrauen mehr Code, obwohl es weniger Gründe für Vertrauen gibt.
Die Kosten für eine schlechte Sucht können als die Summe aller möglichen schlechten Ergebnisse in einer Reihe des Preises jedes schlechten Ergebnisses multipliziert mit seiner Wahrscheinlichkeit (Risiko) angesehen werden.

Der Preis für ein schlechtes Ergebnis hängt vom Kontext ab, in dem die Abhängigkeit verwendet wird. Am einen Ende des Spektrums befindet sich ein persönliches Hobbyprojekt, bei dem der Preis für die meisten schlechten Ergebnisse nahe Null liegt: Sie haben nur Spaß, Fehler haben keine wirklichen Auswirkungen, außer ein wenig mehr Zeit, und das Debuggen kann sogar Spaß machen. Somit ist die Wahrscheinlichkeit eines Risikos nahezu irrelevant: Sie wird mit Null multipliziert. Am anderen Ende des Spektrums befindet sich Produktionssoftware, die jahrelang unterstützt werden muss. Hier können die Kosten für die Abhängigkeit sehr hoch sein: Server können fallen, vertrauliche Daten können offengelegt werden, Kunden können leiden, Unternehmen können sogar bankrott gehen. In der Produktion ist es viel wichtiger, das Risiko eines schwerwiegenden Ausfalls zu bewerten und zu minimieren.
Unabhängig vom erwarteten Preis gibt es einige Ansätze, um das Risiko des Hinzufügens von Abhängigkeiten zu bewerten und zu verringern. Es ist wahrscheinlich, dass Paketmanager optimiert werden sollten, um diese Risiken zu verringern, während sie sich bisher darauf konzentriert haben, die Kosten für das Herunterladen und Installieren zu senken.
Abhängigkeitsprüfung
Sie würden keinen Entwickler einstellen, von dem Sie noch nie gehört haben und von dem Sie nichts wissen. Zuerst erfahren Sie etwas über ihn: Überprüfen Sie die Links, führen Sie ein Interview und so weiter. Bevor Sie von dem Paket abhängen, das Sie im Internet gefunden haben, sollten Sie sich auch ein wenig über dieses Paket informieren.
Eine grundlegende Überprüfung kann eine Vorstellung von der Wahrscheinlichkeit von Problemen geben, wenn versucht wird, diesen Code zu verwenden. Wenn während der Inspektion kleinere Probleme festgestellt werden, können Sie Maßnahmen ergreifen, um diese zu beseitigen. Wenn die Prüfung schwerwiegende Probleme aufzeigt, ist es möglicherweise besser, das Paket nicht zu verwenden: Möglicherweise finden Sie ein geeigneteres, oder Sie müssen es selbst entwickeln. Denken Sie daran, dass Open Source-Pakete von Autoren in der Hoffnung veröffentlicht werden, dass sie nützlich sind, ohne jedoch die Benutzerfreundlichkeit oder den Support zu garantieren. Im Falle eines Produktionsfehlers liegt es an Ihnen, das Debugging durchzuführen. In der ersten
GNU General Public License wurde gewarnt: „Das gesamte Risiko, das mit der Qualität und Leistung des Programms verbunden ist, liegt bei Ihnen. Wenn sich herausstellt, dass das Programm fehlerhaft ist, tragen Sie die Kosten für alle erforderlichen Wartungs-, Reparatur- oder Korrekturarbeiten. “
Als nächstes skizzieren wir einige Überlegungen zur Überprüfung des Pakets und zur Entscheidung, ob Sie davon abhängen möchten.
Design
Ist die Paketdokumentation klar? Hat die API ein klares Design? Wenn Autoren einer Person die API und das Design gut erklären können, erhöht dies die Wahrscheinlichkeit, dass sie auch die Implementierung des Computers im Quellcode gut erklären. Das Schreiben von Code für eine klare, gut gestaltete API ist einfacher, schneller und wahrscheinlich weniger fehleranfällig. Haben die Autoren dokumentiert, was sie vom Client-Code erwarten, um mit zukünftigen Updates kompatibel zu sein? (Beispiele sind
C ++ - und
Go- Kompatibilitätsdokumente).
Codequalität
Ist der Code gut geschrieben? Lesen Sie einige Ausschnitte. Scheinen die Autoren vorsichtig, gewissenhaft und konsequent zu sein? Sieht es aus wie der Code, den Sie debuggen möchten? Möglicherweise müssen Sie dies tun.
Entwickeln Sie Ihre eigenen systematischen Methoden zur Überprüfung der Codequalität. Etwas Einfaches, wie das Kompilieren in C oder C ++ mit
-Wall
wichtigen Compiler-Warnungen (z. B.
-Wall
), kann eine Vorstellung davon geben, wie ernsthaft die Entwickler daran gearbeitet haben, verschiedene undefinierte Verhaltensweisen zu vermeiden. Neuere Sprachen wie Go, Rust und Swift verwenden das
unsafe
Schlüsselwort, um Code zu kennzeichnen, der gegen das Typsystem verstößt. Schauen Sie, wie viel unsicherer Code es gibt. Fortgeschrittenere semantische Tools wie
Infer oder
SpotBugs sind ebenfalls nützlich. Linters sind weniger nützlich: Sie sollten Standardtipps zu Themen wie dem Klammerstil ignorieren und sich auf semantische Themen konzentrieren.
Vergessen Sie nicht die Entwicklungsmethoden, mit denen Sie möglicherweise nicht vertraut sind. Beispielsweise wird die SQLite-Bibliothek als einzelne Datei mit 200.000 Code und einem Header von 11.000 Zeilen geliefert - als Ergebnis des Zusammenführens mehrerer Dateien. Die Größe dieser Dateien löst sofort eine rote Fahne aus, aber eine gründlichere Untersuchung führt zum eigentlichen Quellcode der Entwicklung: einem traditionellen Dateibaum mit mehr als hundert Quell-C-Dateien, Tests und Support-Skripten. Es stellt sich heraus, dass die Einzeldateiverteilung automatisch aus den ursprünglichen Quellen erstellt wird: Dies ist für Endbenutzer einfacher, insbesondere für diejenigen, die keine Abhängigkeitsmanager haben. (Kompilierter Code funktioniert auch schneller, da der Compiler mehr Optimierungsoptionen sieht.)
Testen
Gibt es irgendwelche Tests im Code? Kannst du sie kontrollieren? Bestehen sie? Tests stellen fest, dass die Hauptfunktionalität des Codes korrekt ist, und signalisieren, dass der Entwickler ernsthaft versucht, ihn beizubehalten. Zum Beispiel enthält der SQLite-Entwicklungsbaum eine unglaublich detaillierte Testsuite mit über 30.000 einzelnen Testfällen. Es gibt eine
Dokumentation für Entwickler , in der die Teststrategie erläutert wird. Wenn es dagegen nur wenige oder gar keine Tests gibt oder wenn die Tests fehlschlagen, ist dies eine schwerwiegende rote Fahne: Zukünftige Änderungen im Paket führen wahrscheinlich zu Regressionen, die leicht erkannt werden können. Wenn Sie auf Tests in Ihrem Code bestehen (richtig?), Müssen Sie Tests für den Code bereitstellen, den Sie an andere weitergeben.
Angenommen, die Tests sind vorhanden, werden ausgeführt und bestanden, können Sie zusätzliche Informationen sammeln, indem Sie Tools ausführen,
um die Codeabdeckung zu analysieren,
die Rennbedingungen zu erkennen , die Speicherzuordnung zu überprüfen und Speicherlecks zu erkennen.
Debuggen
Finden Sie den Bug-Tracker für dieses Paket. Gibt es viele offene Fehlermeldungen? Wie lange sind sie schon offen? Wie viele Fehler wurden behoben? Wurden kürzlich Fehler behoben? Wenn es viele offene Fragen zu echten Fehlern gibt, insbesondere wenn diese lange nicht geschlossen wurden, ist dies ein schlechtes Zeichen. Auf der anderen Seite ist es großartig, wenn Fehler selten sind und schnell behoben werden.
Unterstützung
Schauen Sie sich die Geschichte der Commits an. Wie lange wurde der Code aktiv gepflegt? Wird es jetzt aktiv unterstützt? Pakete, die über einen langen Zeitraum aktiv unterstützt wurden, werden wahrscheinlich weiterhin unterstützt. Wie viele Leute arbeiten an dem Paket? Viele Pakete sind persönliche Projekte, die Entwickler in ihrer Freizeit zur Unterhaltung erstellen. Andere sind das Ergebnis von Tausenden von Arbeitsstunden für eine Gruppe bezahlter Entwickler. Im Allgemeinen beheben Pakete des zweiten Typs Fehler schneller, führen ständig neue Funktionen ein und werden im Allgemeinen besser unterstützt.
Andererseits ist ein Code wirklich „perfekt“. Beispielsweise muss
escape-string-regexp
aus NPM möglicherweise nie wieder geändert werden.
Verwenden Sie
Wie viele Pakete hängen von diesem Code ab? Paketmanager geben häufig solche Statistiken an, oder Sie können im Internet sehen, wie oft andere Entwickler dieses Paket erwähnen. Eine größere Anzahl von Benutzern bedeutet zumindest, dass der Code für viele recht gut funktioniert und Fehler darin schneller bemerkt werden. Eine weit verbreitete Verwendung ist auch eine teilweise Garantie für den fortgesetzten Service: Wenn ein weit verbreitetes Paket seinen Betreuer verliert, ist es sehr wahrscheinlich, dass ein interessierter Benutzer seine Rolle übernimmt.
Beispielsweise sind Bibliotheken wie PCRE, Boost oder JUnit unglaublich weit verbreitet. Dies macht es wahrscheinlicher - obwohl dies sicherlich nicht garantiert -, dass die Fehler, auf die Sie möglicherweise gestoßen sind, bereits behoben sind, weil andere vor Ihnen auf sie gestoßen sind.
Sicherheit
Funktioniert dieses Paket mit unsicheren Eingaben? Wenn ja, wie widerstandsfähig ist es gegen schädliche Daten? Hat er Fehler, die in der
National Vulnerability Database (NVD) erwähnt werden ?
Als Jeff Dean und ich 2006 anfingen, an der
Google-Codesuche (
grep
für öffentliche Codebasen) zu arbeiten, schien die beliebte Bibliothek für reguläre Ausdrücke PCRE die naheliegende Wahl zu sein. In einem Gespräch mit dem Google-Sicherheitsteam haben wir jedoch festgestellt, dass PCRE seit langem Probleme hat, z. B. Pufferüberläufe, insbesondere im Parser. Davon waren wir selbst überzeugt, als wir nach PCRE in NVD suchten. Diese Entdeckung führte nicht sofort dazu, dass wir PCRE aufgaben, sondern ließ uns genauer über Tests und Isolation nachdenken.
Lizenzierung
Ist der Code korrekt lizenziert? Hat er überhaupt eine Lizenz? Ist die Lizenz für Ihr Projekt oder Unternehmen akzeptabel? Ein erstaunlicher Teil der GitHub-Projekte hat keine eindeutige Lizenz. Ihr Projekt oder Unternehmen kann zusätzliche Abhängigkeiten für Abhängigkeitslizenzen festlegen. Beispielsweise
verbietet Google
die Verwendung von Code unter Lizenzen wie AGPL (zu eng) und Typ WTFPL (zu vage).
Abhängigkeiten
Hat dieses Paket seine eigenen Abhängigkeiten? Mängel bei indirekten Abhängigkeiten sind ebenso schädlich wie Nachteile bei direkten Abhängigkeiten. Paketmanager können alle transitiven Abhängigkeiten eines bestimmten Pakets auflisten. Jede dieser Abhängigkeiten sollte idealerweise wie in diesem Abschnitt beschrieben überprüft werden. Ein Paket mit vielen Abhängigkeiten erfordert viel Arbeit.
Viele Entwickler haben sich noch nie die vollständige Liste der transitiven Abhängigkeiten ihres Codes angesehen und wissen nicht, wovon sie abhängen. Beispielsweise stellte die NPM-Benutzergemeinschaft im März 2016 fest, dass viele beliebte Projekte - einschließlich Babel, Ember und React - indirekt von einem winzigen Paket abhängen, das als
left-pad
einer 8-Zeilen-Funktion bezeichnet wird. Sie entdeckten dies, als der Autor von
left-pad
das Paket aus NPM entfernte und
versehentlich die meisten Assemblys der Node.js-Benutzer beschädigte. Und das
left-pad
in dieser Hinsicht kaum außergewöhnlich. Zum Beispiel hängen 30% der 750.000 Pakete in NPM - zumindest indirekt - von
escape-string-regexp
. Durch die Anpassung der Beobachtung verteilter Systeme durch Leslie Lamport kann der Paketmanager leicht eine Situation schaffen, in der ein Paketfehler, dessen Existenz Sie nicht einmal kannten, Ihren eigenen Code unbrauchbar machen könnte.
Suchtprüfung
Der Überprüfungsprozess sollte das Ausführen eigener Pakettests umfassen. Wenn das Paket den Test bestanden hat und Sie sich entscheiden, Ihr Projekt davon abhängig zu machen, sollte der nächste Schritt darin bestehen, neue Tests zu schreiben, die sich speziell auf die Funktionalität Ihrer Anwendung konzentrieren. Diese Tests werden häufig als kurze eigenständige Programme gestartet, um sicherzustellen, dass Sie die Paket-API verstehen und das tun, was Sie denken (wenn Sie nicht verstehen oder nicht das tun, was Sie benötigen, hören Sie sofort auf!). Dann lohnt es sich, diese Programme in automatisierte Tests umzuwandeln, die mit neuen Versionen des Pakets ausgeführt werden. Wenn Sie einen Fehler finden und einen potenziellen Fix haben, können Sie diese Tests für ein bestimmtes Projekt einfach neu starten und sicherstellen, dass der Fix nichts anderes beschädigt hat.
Besondere Aufmerksamkeit sollte den Problembereichen gewidmet werden, die bei der Baseline-Überprüfung ermittelt wurden. Bei der Codesuche wussten wir aus früheren Erfahrungen, dass PCRE manchmal lange dauert, um bestimmte reguläre Ausdrücke auszuführen. Unser ursprünglicher Plan war es, separate Thread-Pools für "einfache" und "komplexe" reguläre Ausdrücke zu erstellen. Einer der ersten Tests war ein Benchmark, bei dem
pcregrep
mit mehreren anderen
grep
Implementierungen verglichen wurde. Als wir feststellten, dass
pcregrep
für einen grundlegenden Testfall 70-mal langsamer war als der schnellste
grep
, begannen wir, unseren Plan für die Verwendung von PCRE zu überdenken. Trotz der Tatsache, dass wir PCRE schließlich vollständig aufgegeben haben, bleibt dieser Test heute in unserer Codebasis.
Abhängigkeitsabstraktion
Die Paketabhängigkeit ist eine Lösung, die Sie in Zukunft deaktivieren können. Vielleicht werden Updates das Paket in eine neue Richtung lenken. Es können schwerwiegende Sicherheitsprobleme auftreten. Vielleicht erscheint die beste Option. Aus all diesen Gründen lohnt es sich, die Migration des Projekts in eine neue Abhängigkeit zu vereinfachen.
Wenn ein Paket von vielen Stellen im Projektquellcode aufgerufen wird, müssen Sie Änderungen an all diesen verschiedenen Stellen vornehmen, um zu einer neuen Abhängigkeit zu wechseln. Schlimmer noch, wenn das Paket in der API Ihres eigenen Projekts dargestellt wird, müssen für die Migration in eine neue Abhängigkeit Änderungen am gesamten Code vorgenommen werden, der Ihre API aufruft. Dies liegt möglicherweise bereits außerhalb Ihrer Kontrolle. Um solche Kosten zu vermeiden, ist es sinnvoll, eine eigene Schnittstelle zusammen mit einem Thin Wrapper zu definieren, der diese Schnittstelle mithilfe einer Abhängigkeit implementiert. Bitte beachten Sie, dass der Wrapper nur das enthalten sollte, was das Projekt von der Abhängigkeit benötigt, und nicht alles, was die Abhängigkeit bietet. Im Idealfall können Sie später eine andere, gleichermaßen geeignete Abhängigkeit ersetzen und nur den Wrapper ändern.Die Migration von Tests für jedes Projekt zur Verwendung der neuen Schnittstelle überprüft die Implementierung der Schnittstelle und der Wrapper und vereinfacht auch das Testen möglicher Ersetzungen für Abhängigkeiten.Für die Codesuche haben wir eine abstrakte Klasse entwickelt Regexp
, die die Codesuchschnittstelle definiert, die von jeder Engine für reguläre Ausdrücke benötigt wird. Dann schrieben sie einen dünnen Wrapper um PCRE, der diese Schnittstelle implementiert. Diese Methode erleichterte das Testen alternativer Bibliotheken und verhinderte die versehentliche Einführung von Wissen über interne PCRE-Komponenten in den Rest des Quellbaums. Dies stellt wiederum sicher, dass es bei Bedarf einfach ist, zu einer anderen Abhängigkeit zu wechseln.Abhängigkeitsisolation
Es kann auch angebracht sein, die Abhängigkeit zur Laufzeit zu isolieren, um den möglichen Schaden zu begrenzen, der durch Fehler verursacht wird. Mit Google Chrome können Benutzer beispielsweise Abhängigkeiten zum Browser-Erweiterungscode hinzufügen. Beim ersten Start von Chrome im Jahr 2008 wurde eine wichtige Funktion eingeführt (jetzt Standard in allen Browsern), mit der jede Erweiterung in einer Sandbox isoliert werden kann, die in einem separaten Prozess des Betriebssystems ausgeführt wird. Ein potenzieller Exploit in einer schlecht geschriebenen Erweiterung hatte keinen automatischen Zugriff auf den gesamten Speicher des Browsersund konnte keine unangemessenen Systemaufrufe tätigen. Für die Codesuche war geplant, den PCRE-Parser zumindest in einer ähnlichen Sandbox zu isolieren, bis wir die PCRE vollständig gelöscht haben. Eine weitere Option wäre heute eine leichte Sandbox auf Hypervisor-Basis wie gVisor . Die Abhängigkeitsisolation verringert die damit verbundenen Risiken bei der Ausführung dieses Codes.Selbst mit diesen Beispielen und anderen vorgefertigten Optionen ist das Isolieren von verdächtigem Code zur Laufzeit immer noch zu kompliziert und wird nur selten durchgeführt. Eine echte Isolation erfordert eine vollständig speichersichere Sprache, ohne auf untypisierten Code zu stoßen. Diese sind nicht nur in völlig unsicheren Sprachen wie C und C ++ komplex, sondern auch in Sprachen, die unsichere Vorgänge einschränken, wie Java, wenn JNI aktiviert ist, oder wie Go, Rust und Swift, wenn Sie Ihre unsicheren Funktionen aktivieren. Selbst in einer speichersicheren Sprache wie JavaScript hat Code häufig Zugriff auf viel mehr, als er benötigt. Im November 2018 stellte sich heraus, dass die neueste Version des npm-Pakets event-stream
(eine funktionale Streaming-API für JavaScript-Ereignisse) verwirrenden Schadcode enthältvor zweieinhalb Monaten hinzugefügt. Der Code sammelte Bitcoin-Geldbörsen von Benutzern der mobilen Copay-Anwendung und erhielt Zugriff auf Systemressourcen, die in keiner Beziehung zur Verarbeitung von Ereignisabläufen stehen. Eine der vielen Möglichkeiten, sich vor solchen Problemen zu schützen, wäre eine bessere Abhängigkeitsisolation.Abbruch der Sucht
Wenn die Sucht zu riskant erscheint und Sie sie nicht isolieren können, besteht die beste Option darin, sie vollständig aufzugeben oder zumindest die problematischsten Teile auszuschließen.Als wir beispielsweise die Risiken von PCRE besser verstanden haben, änderte sich unser Plan für die Google-Codesuche von "PCRE-Bibliothek direkt verwenden" zu "PCRE verwenden, aber den Parser in die Sandbox stellen" und dann zu "Neuen Parser für reguläre Ausdrücke schreiben, aber die PCRE-Engine speichern". dann in "Schreiben Sie einen neuen Parser und verbinden Sie ihn mit einer anderen, effizienteren Open Source-Engine." Später haben Jeff Dean und ich auch den Motor neu geschrieben, sodass keine Abhängigkeiten mehr vorhanden waren, und wir haben das Ergebnis entdeckt: RE2 .Wenn Sie nur einen kleinen Teil der Abhängigkeit benötigen, ist es am einfachsten, eine Kopie von dem zu erstellen, was Sie benötigen (natürlich unter Beachtung des relevanten Urheberrechts und anderer rechtlicher Hinweise). Sie übernehmen die Verantwortung für Fehlerkorrektur, Wartung usw., sind aber auch vollständig von größeren Risiken isoliert. In der Go-Entwickler-Community gibt es ein Sprichwort : "Ein bisschen Kopieren ist besser als ein bisschen Abhängigkeit."Abhängigkeitsaktualisierung
Lange Zeit war die allgemein anerkannte Weisheit in der Software: "Wenn es funktioniert, berühren Sie nichts." Das Aktualisieren birgt das Risiko, dass neue Fehler auftreten. ohne Belohnung - wenn Sie keine neue Funktion benötigen, warum das Risiko eingehen? Dieser Ansatz ignoriert zwei Aspekte. Erstens die Kosten für ein schrittweises Upgrade. In der Software lässt sich die Komplexität der Änderungen am Code nicht linear skalieren: Zehn kleine Änderungen sind weniger Arbeit und einfacher als eine entsprechende große Änderung. Zweitens die Schwierigkeit, bereits korrigierte Fehler zu erkennen. Insbesondere im Sicherheitskontext, in dem bekannte Fehler aktiv ausgenutzt werden, erhöht sich jeden Tag ohne Aktualisierung das Risiko, dass Angreifer Fehler im alten Code ausnutzen können.Betrachten Sie zum Beispiel die Geschichte von Equifax aus dem Jahr 2017, die Führungskräfte in Zeugnissen vor dem Kongress ausführlich beschrieben haben. Am 7. März wurde eine neue Sicherheitslücke in Apache Struts entdeckt und eine gepatchte Version veröffentlicht. Am 8. März erhielt Equifax die US-CERT-Benachrichtigung über die Notwendigkeit, die Verwendung von Apache Struts zu aktualisieren. Equifax startete am 9. und 15. März einen Scan des Quellcodes und des Netzwerks. Kein einziger Scan hat gefährdete Webserver gefunden, die im Internet geöffnet sind. Am 13. Mai fanden Angreifer solche Server, die Equifax-Experten nicht fanden. Sie nutzten die Apache Struts-Sicherheitslücke, um das Equifax-Netzwerk zu hacken, und stahlen in den nächsten zwei Monaten detaillierte persönliche und finanzielle Informationen über 148 Millionen Menschen. Schließlich bemerkte Equifax am 29. Juli einen Hack und kündigte ihn am 4. September öffentlich an. Bis Ende September waren der CEO von Equifax sowie CIO und CSO zurückgetreten, und im Kongress hatte eine Untersuchung begonnen.Die Erfahrung von Equifax führt dazu, dass Paketmanager zwar die Versionen kennen, die sie während des Builds verwenden, Sie jedoch andere Mechanismen benötigen, um diese Informationen während der Bereitstellung in der Produktion zu verfolgen. Für die Go-Sprache experimentieren wir damit, das Manifest-Manifest automatisch in jede Binärdatei aufzunehmen, damit die Bereitstellungsprozesse die Binärdateien nach Abhängigkeiten durchsuchen können, die aktualisiert werden müssen. Go stellt diese Informationen auch zur Laufzeit zur Verfügung, sodass Server auf Datenbanken mit bekannten Fehlern zugreifen und diese unabhängig an das Überwachungssystem melden können, wenn sie aktualisiert werden müssen.Ein schnelles Update ist wichtig, aber das Aktualisieren bedeutet, dass dem Projekt neuer Code hinzugefügt wird. Dies sollte bedeuten, dass die Risikobewertung der Abhängigkeitsverwendung basierend auf der neuen Version aktualisiert wird. Zumindest möchten Sie die Unterschiede sehen, die die Änderungen zeigen, die von der aktuellen Version zu den aktualisierten Versionen vorgenommen wurden, oder zumindest die Versionshinweise lesen, um die wahrscheinlichsten Problembereiche im aktualisierten Code zu identifizieren. Wenn sich viele Codes ändern, sodass die Unterschiede schwer zu verstehen sind, können Sie diese Informationen auch in die Aktualisierung Ihrer Risikobewertung einbeziehen.Darüber hinaus müssen Sie speziell für das Projekt geschriebene Tests erneut ausführen, um sicherzustellen, dass das aktualisierte Paket für das Projekt mindestens genauso geeignet ist wie die frühere Version. Es ist auch sinnvoll, Ihre eigenen Pakettests erneut auszuführen. Wenn das Paket seine eigenen Abhängigkeiten hat, verwendet die Projektkonfiguration möglicherweise andere Versionen dieser Abhängigkeiten (älter oder neuer) als die von den Autoren des Pakets verwendeten. Durch Ausführen eigener Pakettests können Sie schnell konfigurationsspezifische Probleme identifizieren.Auch hier müssen Updates nicht vollautomatisch sein. Stellen Sie vor dem Bereitstellen aktualisierter Versionen sicher, dass diese für Ihre Umgebung geeignet sind .Wenn der Aktualisierungsprozess die erneute Ausführung der bereits geschriebenen Integrations- und Qualifizierungstests umfasst, ist die Verzögerung bei der Aktualisierung in den meisten Fällen riskanter als eine schnelle Aktualisierung.Das Fenster für wichtige Sicherheitsupdates ist besonders klein. Nachdem Equifax gehackt worden war, fanden forensische Sicherheitsteams Hinweise darauf, dass Angreifer (möglicherweise andere) die Apache Struts-Sicherheitsanfälligkeit auf den betroffenen Servern am 10. März, nur drei Tage nach ihrer Veröffentlichung, erfolgreich ausnutzten. Aber sie haben dort nur ein Team ins Leben gerufen whoami
.Beobachten Sie Ihre Sucht
Auch nach alledem ist die Arbeit noch nicht beendet. Es ist wichtig, Abhängigkeiten weiterhin zu überwachen und in einigen Fällen sogar aufzugeben.Stellen Sie zunächst sicher, dass Sie weiterhin bestimmte Versionen von Paketen verwenden. Mit den meisten Paketmanagern können Sie jetzt den kryptografischen Hash des erwarteten Quellcodes für eine bestimmte Version des Pakets einfach oder sogar automatisch aufzeichnen und diesen Hash dann überprüfen, wenn Sie das Paket erneut auf einen anderen Computer oder in einer Testumgebung herunterladen. Dadurch wird sichergestellt, dass der Build denselben Abhängigkeitsquellcode verwendet, den Sie getestet und getestet haben. Solche Kontrollen verhinderten den Angreiferevent-stream
Fügen Sie automatisch schädlichen Code in die bereits veröffentlichte Version 3.3.5 ein. Stattdessen musste der Angreifer eine neue Version 3.3.6 erstellen und auf die Aktualisierung warten (ohne die Änderungen sorgfältig zu prüfen).Es ist auch wichtig, das Auftreten neuer indirekter Abhängigkeiten zu überwachen: Aktualisierungen können leicht neue Pakete einführen, von denen der Erfolg Ihres Projekts jetzt abhängt. Sie verdienen auch Ihre Aufmerksamkeit. In diesem Fall wurde der event-stream
Schadcode in einem anderen Paket versteckt flatMap-stream
, das in der neuen Version event-stream
als neue Abhängigkeit hinzugefügt wurde.Kriechende Abhängigkeiten können sich auch auf die Projektgröße auswirken. Während der Entwicklung von Google Sawzall- JIT-Verarbeitungssprache von Protokollen - Zu unterschiedlichen Zeiten stellten die Autoren fest, dass die Hauptinterpreter-Binärdatei nicht nur JIT Sawzall, sondern auch (nicht verwendete) PostScript-, Python- und JavaScript-Interpreter enthält. Jedes Mal stellte sich heraus, dass der Schuldige nicht verwendete Abhängigkeiten waren, die von einer Sawzall-Bibliothek deklariert wurden, zusammen mit der Tatsache, dass das Google-Build-System die neue Abhängigkeit vollautomatisch verwendete. Aus diesem Grund gibt der Go-Compiler beim Importieren eines nicht verwendeten Pakets einen Fehler aus.Das Aktualisieren ist der natürliche Zeitpunkt, um Ihre Entscheidung zur Verwendung einer sich ändernden Abhängigkeit zu überarbeiten. Es ist auch wichtig, jede Sucht, die dies nicht tut , regelmäßig zu überprüfenändert sich. Scheint es plausibel, dass keine Sicherheitsprobleme oder andere Fehler behoben werden müssen? Wird das Projekt aufgegeben? Vielleicht ist es Zeit, einen Ersatz für diese Abhängigkeit zu planen.Es ist auch wichtig, das Sicherheitsprotokoll jeder Abhängigkeit zu überprüfen. Beispielsweise hat Apache Struts in den Jahren 2016, 2017 und 2018 schwerwiegende Sicherheitslücken bei der Remotecodeausführung aufgedeckt. Selbst wenn Sie viele Server haben, die es starten und schnell aktualisieren, legt eine solche Erfolgsbilanz nahe, ob es sich überhaupt lohnt, es zu verwenden.Fazit
Die Ära der Wiederverwendung von Software ist endlich angebrochen, und ich möchte die Vorteile nicht herunterspielen: Sie hat den Entwicklern eine äußerst positive Veränderung gebracht. Wir haben diese Transformation jedoch angenommen, ohne die möglichen Konsequenzen vollständig zu berücksichtigen. Frühere Gründe, Abhängigkeiten zu vertrauen, verlieren gleichzeitig an Relevanz, wenn wir mehr Abhängigkeiten als je zuvor haben.Die kritische Analyse spezifischer Abhängigkeiten, die ich in diesem Artikel beschrieben habe, stellt einen erheblichen Arbeitsaufwand dar und bleibt eher die Ausnahme als die Regel. Aber ich bezweifle, dass es Entwickler gibt, die wirklich hart daran arbeiten, dies für jede mögliche neue Sucht zu tun. Ich habe nur einen Teil dieser Arbeit für einige meiner eigenen Abhängigkeiten gemacht. Grundsätzlich läuft die gesamte Lösung auf Folgendes hinaus: „Mal sehen, was passiert.“ Zu oft scheint etwas mehr zu viel Aufwand.Copay- und Equifax-Angriffe sind jedoch klare Warnungen vor echten Problemen bei der Verwendung von Softwareabhängigkeiten. Wir dürfen Warnungen nicht ignorieren. Ich biete drei allgemeine Empfehlungen an.- . , , , . , .
- . , , . , , . , , , .
- . . . , . , , . , , API. .
Es gibt viele gute Software. Lassen Sie uns zusammenarbeiten und herausfinden, wie Sie es sicher verwenden können.