Skip to main content

Die 5 Dimensionen einer npm-Abhängigkeit

Artikel von

16. Juni 2016

0 Min. Lesezeit

Wir sprechen oft über die wachsende Zahl von npm-Abhängigkeiten und darüber, wie sie uns einerseits produktiver und schneller machen, andererseits aber anfällig und potenziell unsicher werden lassen. Doch was genau ist eine npm-Abhängigkeit?

Bei Snyk konzentriert sich unser Produkt auf die Absicherung von Abhängigkeiten. Deshalb mussten wir zunächst genau definieren, was eine Abhängigkeit ist. In diesem Beitrag beleuchten wir die verschiedenen Dimensionen einer Abhängigkeit, teilen unsere Erkenntnisse aus dem Versuch, eine einfache Taxonomie zu entwickeln, und helfen Ihnen dabei, die möglichen Gruppierungen zu verstehen.

Grundlegende Definition: Code, von dem Sie abhängig sind

In der einfachsten Definition ist eine Abhängigkeit einfach ein Codepaket, von dem Ihre Anwendung abhängt. Ohne diesen Code funktioniert Ihre Anwendung nicht richtig und lässt sich möglicherweise nicht einmal erstellen.

Daher wirkt sich jede Abhängigkeit – ganz gleich, wie Sie sie organisieren – in irgendeiner Weise auf die Funktionalität, Zuverlässigkeit und Sicherheit Ihrer Anwendung aus. Sehen wir uns vor diesem Hintergrund an, wie wir sie unterteilen können.

Dimension 1: Dev vs. Prod

Der erste und klarste Abhängigkeitstyp ist die Unterscheidung zwischen Dev und Prod. In der Datei package.json werden Produktionsabhängigkeiten ausdrücklich als dependencies aufgeführt, während Abhängigkeiten, die nur während der Entwicklung verwendet werden, devDependencies heißen.

Der Befehl npm install installiert standardmäßig sowohl Dev- als auch Prod-Abhängigkeiten für die aktuelle App, aber nur Produktionsabhängigkeiten für Pakete, die von npm heruntergeladen werden.

Das npm-Paket util verwendet beispielsweise die folgenden Abhängigkeitsbereiche in seiner package.json-Datei:

{
  "name": "util",
...
  "dependencies": {
    "inherits": "2.0.1"
  },
...
  "devDependencies": {
    "zuul": "~1.0.9"
  },
...
}

Wenn Sie npm install util ausführen, wird nur die (Produktions-)Abhängigkeit inherits mitinstalliert. Wenn Sie jedoch das Repository klonen, node-util verwenden und im geklonten Ordner npm install ausführen, werden sowohl inherits als auch zuul installiert.

Wie bereits erwähnt, nimmt eine Anwendung diese Trennung ausdrücklich vor. Die Logik dahinter ist recht einfach: Wird die Abhängigkeit benötigt, damit die Anwendung ausgeführt werden kann, sollte sie eine Produktionsabhängigkeit sein. Wird sie nur für Tests oder den Build benötigt, sollte sie eine Dev-Abhängigkeit sein. Bei der Suche nach Schwachstellen sind Dev-Abhängigkeiten weniger relevant. Deshalb testet Snyk standardmäßig nur Produktionsabhängigkeiten (Sie können dies jedoch mit dem Flag --dev ändern).

Beachten Sie, dass peerDependencies und optionalDependencies ebenfalls Produktionsabhängigkeiten sind, allerdings mit Besonderheiten hinsichtlich des Zeitpunkts und der Art ihrer Installation. Darauf gehen wir ein, wenn wir die Dimension „Logisch vs. auf dem Datenträger“ behandeln.

Diagramm zur Code-Zusammensetzung von WP-Calypso: 15,67 % Projektcode, 61,98 % Abhängigkeitscode und 22,35 % Code aus Entwicklungsabhängigkeiten

Das obige Bild zeigt ein Beispiel für das Verhältnis von Dev- zu Prod-Abhängigkeiten, erstellt von bitHound.

Dimension 2: Direkt vs. indirekt

Einige Ihrer Abhängigkeiten sind direkt (auch primär genannt) und werden ausdrücklich in Ihrer package.json-Datei angefordert. Die meisten Abhängigkeiten sind jedoch indirekt (auch sekundär genannt): Sie werden von einer direkten Abhängigkeit (oder einer anderen indirekten Abhängigkeit) eingebunden, um deren Aufgabe zu erfüllen. Bei den meisten Anwendungen macht die große Mehrheit der gesamten Abhängigkeitenliste indirekte Abhängigkeiten aus. Nur sehr wenige Anwendungen verwenden beispielsweise left-pad direkt, einige bekannte Anwendungen (wie babel und node) jedoch schon. Dadurch wurde left-pad für sehr viele Anwendungen zu einer indirekten Abhängigkeit, weshalb seine Entfernung aus der Registry so weitreichende Folgen hatte.

Wenn ein Paket weit von Ihrer App entfernt und tief im Abhängigkeitsbaum verschachtelt ist, kann es leicht unbemerkt bleiben oder ganz in Vergessenheit geraten. Doch auch entfernte Abhängigkeiten sind Abhängigkeiten. Die Entfernung von left-pad legte Anwendungen lahm, auch solche, die gar nicht wussten, dass sie es verwendeten. Wer die Lizenz einer tief verschachtelten Abhängigkeit nicht einhält, kann weiterhin rechtliche Probleme bekommen; und eine Schwachstelle in einer weit entfernten indirekten Abhängigkeit kann Angreifern nach wie vor Zugang verschaffen.

Dimension 3: Paket vs. Version

Angenommen, Sie verwenden das Paket request in Version 2.11.3. Was ist dann Ihre Abhängigkeit? Ist es request, das Paket, oder request@2.11.3, also die konkrete Version?

Die vollständige Antwort lautet: beides. Sie sind eindeutig von request@2.11.3 abhängig. Diese Version steht für ein unveränderliches Programm, das Sie eingebunden haben und in Ihrem Code verwenden. Fehler in diesem Paket, wie etwa diese Sicherheitslücke zur Offenlegung des Remote-Speichers, wirken sich auf Ihren Code aus. Außerdem müssen Sie die MIT-Lizenz einhalten, unter der es bereitgestellt wurde, und so weiter.

Sie sind jedoch auch vom Paketprojekt request abhängig. Wenn Sie es über einen semver-Bereich verwenden, verlassen Sie sich darauf, dass die Autorinnen und Autoren keine inkompatible Änderung in einem Minor-Release veröffentlichen. Aus Sicherheitssicht verlassen Sie sich darauf, dass sie weder ihre npm- noch ihre GitHub-Zugangsdaten preisgeben, vorab auf Sicherheitsprobleme testen und offengelegte Schwachstellen schnell beheben. Mit der Zeit sind Sie außerdem darauf angewiesen, dass das Projekt und seine Autorinnen und Autoren es gut pflegen, Fehler beheben und zeitnah Funktionen ergänzen. Sie können dieses Risiko verringern, indem Sie mit shrinkwrap die verwendeten Paketversionen fixieren oder Abhängigkeiten mit einbinden. In beiden Fällen erhalten Sie jedoch keine neuen Funktionen und Fehlerbehebungen mehr.

Sowohl das Paket als auch die Version sind Ihre Abhängigkeiten, doch Sie sind auf sehr unterschiedliche Weise von ihnen abhängig. Daher wäre es sinnvoll, ihnen unterschiedliche Bezeichnungen zu geben. Leider gibt es keine eindeutige Bezeichnung für eine Kombination aus Paket und Version, und der Begriff Paket wird für beides gleichermaßen verwendet.

Wenn wir bei Snyk von einer Abhängigkeit sprechen, meinen wir damit in der Regel eine Kombination aus Paket und Version, zum Beispiel request@2.11.3. Wenn wir das Paket meinen, sprechen wir ausdrücklich von einem abhängigen Paket. Die Taxonomie ist hier allerdings knifflig. Wir versuchen zwar, uns an diese Richtlinien zu halten, sagen aber manchmal einfach „Paket“ und überlassen es den Lesenden, anhand des Kontexts zu entscheiden, was gemeint ist …

Paketversionsauswahl mit „request“, dem Bereich „2.x“ und einer Liste passender semantischer Versionen der 2.x-Reihe.

Pakete wie request gibt es in vielen Versionen. Sie sind darauf angewiesen, dass jede Version und das Projekt gut verwaltet werden.

Dimension 4: Logisch vs. auf dem Datenträger

Bisher ging es bei allem um logische Abhängigkeiten – also darum, wie Ihr Abhängigkeitsbaum konzeptionell aussieht. Der Baum kann sich jedoch erheblich verändern, wenn er tatsächlich auf den Datenträger heruntergeladen wird. Das liegt zum Teil an Peer- und optionalen Abhängigkeiten, die teilweise nur bei Bedarf installiert werden, noch stärker wirkt sich jedoch die Deduplizierung in npm3 aus.

Sehen wir uns das Paket inflight an. Hier ist der logische Abhängigkeitsbaum dafür:

inflight@1.0.5
├─┬ once@1.3.3
│ └── wrappy@1.0.2
└── wrappy@1.0.2

Die Abhängigkeit wrappy@1.0.2 wird sowohl als direkte Abhängigkeit als auch indirekt über once@1.3.3 verwendet. Wenn wir das inflight-Repository klonen und npm install --production sowie npm ls ausführen, erhalten wir Folgendes:

inflight@1.0.5
├── once@1.3.3
└── wrappy@1.0.2

Wie Sie sehen, wird wrappy@1.0.2 nur einmal aufgeführt. Das ist das Ergebnis der Deduplizierung in npm3: Sie erkennt das wiederholte Paket und vermeidet eine weitere Kopie auf dem Datenträger. Auch npm2 führt standardmäßig eine minimale Deduplizierung durch; explizit ausführen lässt sie sich mit npm dedupe.

Unser Beispiel ist einfach gehalten. Mit Semver-Bereichen und wiederholten npm-Installationen kann es jedoch kompliziert und weniger vorhersehbar werden. Mit snyk-resolve können Sie Ihre logischen Abhängigkeiten mit den tatsächlich auf dem Datenträger installierten vergleichen. Remy hat darüber in diesem Blog geschrieben.

Die wichtigste Erkenntnis zu dieser Dimension: Ihre logischen und Ihre auf dem Datenträger vorhandenen Abhängigkeiten können voneinander abweichen. Welche Abhängigkeiten auf dem Datenträger vorhanden sind, hängt von der Installationslogik und der Reihenfolge ab. Prüfen Sie unbedingt, was tatsächlich für die gesamte Anwendung installiert wurde – nicht nur die Logik jeder einzelnen direkten Abhängigkeit.

Dimension 5: Abhängigkeitspfad vs. eindeutige Abhängigkeit

Nachdem wir unsere Abhängigkeiten definiert haben, geht es in der letzten Dimension darum, sie zu zählen. Betrachten Sie den folgenden logischen Abhängigkeitsbaum:

app@1.2.3
├─┬ A@1.0.0
│ └── B@1.0.0
├─┬ C@1.0.0
│ └── B@2.0.0
└── B@2.0.0

Anhand dieses Baums können wir sagen, dass die App 3 abhängige Pakete hat: A, B und C. Außerdem können wir sagen, dass sie 4 Abhängigkeiten auf dem Datenträger hat (einschließlich Version): A@1.0.0, B@1.0.0, B@2.0.0 und C@1.0.0 – bei der Deduplizierung würden redundante Einträge vermieden. Aber wie viele logische Abhängigkeiten hat die App? Sind es 4 Abhängigkeiten, eine für jede Kombination aus Paket und Version, oder 5 Abhängigkeiten, eine für jeden Knoten im Abhängigkeitsbaum?

Um die beiden Fälle auseinanderzuhalten, ist es hilfreich zu sagen, dass diese App 4 eindeutige Abhängigkeiten und 5 Abhängigkeits-Pfade hat. Wenn beispielsweise B@2.0.0 eine bekannte Schwachstelle aufweist, sprechen wir bei Snyk von einer bekannten Schwachstelle, aber zwei verwundbaren Pfaden.

Zusammenfassung

Zusammenfassend lässt sich sagen: Ihre Abhängigkeiten haben mehrere Dimensionen, die sich jeweils für unterschiedliche Zwecke eignen. Wenn wir über Abhängigkeiten sprechen, sollten wir nach Möglichkeit dieselbe Taxonomie verwenden, damit die Kommunikation reibungslos verläuft.

Hier noch einmal die 5 Dimensionen als Übersicht:

  1. Dev vs. Prod: Ihre App benötigt Dev-Abhängigkeiten für Build und Tests und Prod-Abhängigkeiten für die Ausführung.

  2. Direkt vs. indirekt: Ihre App benötigt ausdrücklich nur direkte Abhängigkeiten. Qualitäts-, Rechts- und Sicherheitsprüfungen sollten jedoch auch die (deutlich zahlreicheren) indirekten Abhängigkeiten berücksichtigen.

  3. Paket vs. Version: Ihre bereitgestellte App wird von der konkreten Version jeder Abhängigkeit beeinflusst. Ihr Projekt ist jedoch darauf angewiesen, dass jedes abhängige Paket weiterhin funktioniert.

  4. Logisch vs. auf dem Datenträger: Der logische Abhängigkeitsbaum Ihrer App kann sich bei der Installation auf dem Datenträger erheblich verändern. Prüfen Sie daher, welche Versionen tatsächlich installiert wurden.

  5. Pfad vs. eindeutig: Achten Sie beim Zählen Ihrer Abhängigkeiten darauf, die Anzahl eindeutiger Abhängigkeiten von der Anzahl der Abhängigkeitspfade zu unterscheiden. So können Sie den Umfang einer Aufgabe oder eines Problems richtig einschätzen.

Hinweis: Weitere ausführliche Informationen und Einblicke finden Sie in diesem Artikel zu npm- und yarn-Paketmanifesten und zur Funktionsweise von Lock-Dateien für Anwendungen und Bibliotheken.

Nachdem Sie nun mit den Begriffen vertraut sind, können Sie mit Snyk Ihre Anwendung testen und herausfinden, wie viele verwundbare Produktionsabhängigkeiten und Abhängigkeitspfade sie möglicherweise verwendet. Außerdem können Sie auf unserer Vulnerability DB nach abhängigen Paketen suchen und prüfen, ob bei ihnen in der Vergangenheit Sicherheitslücken aufgetreten sind.

Starten Sie mit Capture-the-Flag

Erfahren Sie in unserem virtuellen On-Demand-Workshop für Einsteiger, wie Sie Capture-the-Flag-Challenges lösen.

Gepostet in: