Ihre private npm-Registry hat eine Schwachstelle | TuxCare

Die „php2shell“-Patches (CVE-2026-109618 und CVE-2026-109619) für PHP am Ende des Lebenszyklus (EOL) sind nun verfügbar. Da die Antworten des Vertriebs aufgrund der hohen Nachfrage derzeit langsamer als gewöhnlich ausfallen, ist der schnellste Weg zu den gepatchten Paketen der Online-Kauf von ELS für Runtimes (wird in einem neuen Tab geöffnet). Lesen Sie die FAQ zu den Patches

Gehen Sie direkt zu einer KI-Zusammenfassung dieses Blogbeitrags

  • Fasse diesen Beitrag mit Perplexity zusammen
  • Fasse diesen Beitrag mit Gemini zusammen

Wichtigste Erkenntnisse

  • Git-URLs und Remote-Tarball-Abhängigkeiten werden von ihrem jeweiligen Speicherort abgerufen, niemals über Ihre Registry.
  • Von den 53.707 Paketversionen, die wir verfolgen, werden 811 Abhängigkeitsspezifikationen außerhalb der Registry aufgelöst.
  • Branch-, Tag- und nicht fixierte Referenzen stellen das eigentliche Risiko dar, da sich ihr Code ohne eine Veröffentlichung ändern kann.
  • Eine Byte-für-Byte-Rekonstruktion belegt die originalgetreue Wiedergabe, nicht die vollständige Übereinstimmung; daher ist ein originalgetreu reproduzierter Escape-Zeichen immer noch ein Escape-Zeichen.

Die meisten Teams, die ein privates npm Die Registries beschreiben es auf dieselbe Weise. Alles läuft über Nexus oder Artifactory, die öffentliche Registry befindet sich hinter einem Proxy, und nichts gelangt auf den Laptop eines Entwicklers oder einen CI-Runner, ohne zuvor die einzige Tür passiert zu haben, die sie kontrollieren.

Das stimmt größtenteils. Gerade dieses „größtenteils“ ist es, worüber es sich zu sprechen lohnt.

Bei der Entwicklung von SecureChain, dem Dienst von TuxCare, der Open-Source-Pakete aus dem Quellcode neu erstellt und mit Herkunftsnachweis bereitstellt, haben wir nach Fällen gesucht, in denen dies nicht zutrifft. Wir haben 53.707 Versionen des npm Pakete im Abhängigkeitsgraphen haben wir jede Abhängigkeit, die angibt, npm um die Registrierungsdatenbank komplett zu umgehen. In diesem Beitrag geht es darum, was wir herausgefunden haben, warum Ihre Registrierungskonfiguration diese Abhängigkeiten nicht umleiten kann und was Sie diese Woche in Ihren eigenen Projekten überprüfen sollten.

Was wir unter einer „Escaping-Abhängigkeit“ verstehen

Eine „Mirror-Escaping“-Abhängigkeit ist eine Abhängigkeit, die nicht mit einem Paketnamen und einem Versionsbereich, sondern mit einer Git-URL, einer Tarball-URL oder einem anderen Remote-Speicherort deklariert wurde, sodass npm Es wird direkt von diesem Speicherort abgerufen, anstatt es über Ihre Registrierung aufzulösen.

Hier ist ein Beispiel. Die Namen wurden geändert, aber die Struktur entspricht genau dem, was wir in den Laufzeitabhängigkeiten eines veröffentlichten Pakets gefunden haben:

"dependencies": {
  "lodash": "^4.15.0",
  "some-markdown-lib": "git://github.com/some-user/some-markdown-lib.git"
}

Die erste Zeile entspricht dem Normalfall. npm fragt die von Ihnen konfigurierte Registrierungsdatenbank nach einem lodash eine Version, die die folgenden Bedingungen erfüllt ^4.15.0. Die zweite Zeile enthält weder eine Version noch einen Commit noch eine Registry. Sie nennt ein Repository auf einem persönlichen GitHub-Konto, und npm holt das, was sich zum Zeitpunkt der Installation gerade auf seinem Standardzweig befindet.

Wenn das git:// Das Präfix wirkt auf dich wie tot – gutes Auge. GitHub hat das nicht authentifizierte Git-Protokoll bereits 2022 deaktiviert. Das bedeutet jedoch nicht, dass die Abhängigkeit inaktiv ist. npm normalisiert GitHub git:// URLs werden auf HTTPS umgeleitet, sodass die Abhängigkeiten in jeder Umgebung, die Git-Abhängigkeiten zulässt, weiterhin aufgelöst und installiert werden. Eine Spezifikation, die bei der Code-Review fehlerhaft erscheint, funktioniert in der Praxis einwandfrei – eine ziemlich gute Möglichkeit, damit etwas jahrelang unbemerkt bleibt.

Warum Ihre Registrierungseinstellungen darauf keinen Einfluss haben

Die registry sich einpendeln .npmrc beantwortet eine Frage. Wenn eine Abhängigkeit aus einem Namen und einem Bereich besteht, wo sollte npm Nachschlagen? Eine Git-URL oder ein HTTPS-Tarball-Link ist keine solche Abhängigkeit. Sie enthält ihren eigenen Speicherort, und npm geht direkt dorthin. Bereichsbezogene Registrierungsüberschreibungen, Authentifizierungstoken, ein Proxy-Repository vor npmjs.org: Das spielt überhaupt keine Rolle, denn npm stellt von vornherein gar keine Registrierungsanforderung für diese Abhängigkeit.

Auch die Tiefe spielt keine Rolle. Wenn ein Paket, das drei Ebenen tiefer in Ihrem Baum liegt, eine solche Deklaration enthält, greift Ihre Installation mitten im Installationsvorgang auf GitHub zu – und zwar von einem Laptop oder Runner aus, von dem Sie angenommen hatten, dass er nur mit Ihrer Registry kommuniziert. Wenn diese Rechner GitHub nicht erreichen können, schlägt die Installation fehl, und so erfahren viele Teams erst, dass es solche Pakete überhaupt gibt. Wenn sie GitHub erreichen können, schlägt nichts fehl und niemand bemerkt etwas.

Lockfiles helfen weniger, als man erwartet. npm Es wird zwar der aufgelöste Commit für eine Git-Abhängigkeit gespeichert, sodass bei wiederholten Installationen anhand derselben Lockdatei derselbe Code verwendet wird. Die erste Auflösung bezieht sich jedoch weiterhin auf den Stand des Branches an jenem Tag, und jeder Fetch erfolgt nach wie vor über GitHub statt über Ihre Registry. Eine Lockdatei fixiert den Inhalt. Sie ändert nichts daran, woher der Inhalt stammt.

Was uns 53.707 Paketversionen verraten haben

Bei diesen 53.707 Paketversionen haben wir 811 Abhängigkeitsspezifikationen gefunden, die außerhalb der Registry aufgelöst werden und sich auf 682 Versionen von 303 verschiedenen Paketen verteilen. Davon entfallen 730 auf GitHub. Der Rest ist eine bunte Mischung aus CDN-Links, Download-Servern von Anbietern, privatem Hosting und einem GitHub-Gist, der als Abhängigkeit herangezogen wurde.

Von den 811 Spezifikationen sind 636 Entwicklungsabhängigkeiten. Nutzer des veröffentlichten Pakets installieren diese normalerweise nicht, allerdings werden sie bei jedem Build des Pakets selbst abgerufen, was wichtig ist, wenn man Wert auf reproduzierbare Builds legt.

Die übrigen 175 befinden sich in Laufzeit- oder optionalen Abhängigkeiten, also in den für Endnutzer relevanten Bereichen. Diese Zahl muss mit einem Sternchen versehen werden, und es ist besser, dies jetzt zu tun, als dass später jemand darauf hinweist. Von diesen 175 stammen 155 aus einem einzigen, längst aufgegebenen CLI-Tool mit Dutzenden veröffentlichter Versionen, von dem jedoch nichts in dem von uns verfolgten Abhängigkeitsgraphen abhängt. Nimmt man dieses eine Tool heraus, bleiben 20 benutzerseitige „Escaping“-Spezifikationen in 15 Paketen übrig.

Zwanzig klingt nach wenig, und das ist es auch. Niemand sollte dies als Epidemie interpretieren. Aber 13 dieser 20 verweisen auf einen Branch oder ein leeres Repository, was bedeutet, dass sich der dahinterstehende Code jederzeit ändern kann. Gerade durch solche kleinen Zahlen bleiben solche Dinge oft unbemerkt. Niemand überprüft die 15 Pakete, von denen man nicht wusste, dass sie etwas Besonderes sind.

Nicht jede Flucht ist gleich gefährlich

Die Form der Brille ist weitaus wichtiger als die Richtung, in die sie zeigt.

Eine an einen vollständigen Commit-SHA gebundene Abhängigkeit ist inhaltsadressiert. Ein Repository-Besitzer kann diese Referenz nicht wie einen Branch oder ein Tag auf einen anderen Commit verschieben. Selbst wenn das Repository morgen den Besitzer wechselt, erhalten Sie also genau das, was zum Zeitpunkt der Festlegung vorhanden war. Was bleibt, ist die Verfügbarkeit sowie die Tatsache, dass die Registry weiterhin umgangen wird. Dabei handelt es sich eher um ein Problem der Eindämmung als um ein Problem der Code-Injektion.

Reine Repository-URLs, Branches, Tags und nach Semver aufgelöste Git-Refs sollten alle als veränderbar behandelt werden. Fehlt ein Ref gänzlich, gilt der Standard-Branch. Ein #branch ref folgt dem Zweig. A #semver: Der Bereich wird zum Zeitpunkt der Installation anhand passender Tags oder Referenzen aufgelöst. Und Tags verdienen einen eigenen Satz, denn viele behandeln sie wie Versionen. Das sind sie aber nicht. Ein Tag ist ein Zeiger, und der Repository-Besitzer kann ihn jederzeit auf einen anderen Commit verschieben, wenn ihm danach ist. Wenn es sich nicht um einen SHA handelt, gehe davon aus, dass er sich ändern kann.

URLs zu externen Tarballs sollten grundsätzlich ebenfalls in den „mutable“-Bucket verschoben werden, es sei denn, ihre Integrität wird anderweitig unabhängig gesichert. Bei den von GitHub generierten Quellcode-Archiven sind die Prüfsummen bereits in der Vergangenheit geändert worden, ohne dass jemand das Repository bearbeitet hat. Daher garantiert selbst ein Link, der unverändert zu sein scheint, nicht, dass die Daten identisch sind.

Bei unserer Erhebung war das Verhältnis sehr unausgewogen. Nach dieser Klassifizierung waren 687 der 811 Spezifikationen veränderbar, während nur 124 an einen SHA gebunden waren. Wir haben nicht gezählt, file:, link:, workspace:, oder relative Pfade überhaupt. Davon gab es 1.061, und wir haben sie ausgeschlossen, da sie niemals mit dem Netzwerk in Berührung kommen und daher nichts entweichen lassen können.

Schauen Sie sich nun an, was im Fall der veränderbaren Version nicht erforderlich ist. Keine Kontoübernahme, kein Typosquatting, kein gestohlenes Veröffentlichungstoken. Der Eigentümer dieses Forks kann an einem Dienstagnachmittag in seinen eigenen Standardzweig pushen, und jede neue Installation, egal wo, greift dies auf. Es gibt keinen Versionssprung, kein Veröffentlichungsereignis und nichts Neues im Register, das ein Scanner überprüfen könnte. Wenn die URL auf eine Domain verweist, deren Verlängerung jemand vergessen hat, braucht man nicht einmal den Eigentümer. Wer auch immer die Domain als Nächstes registriert, kann ein gültiges Zertifikat dafür erhalten und entscheiden, was Ihre Installation herunterlädt.

Ein umweltfreundliches Bauprojekt reicht nicht aus

Das war der Punkt, der uns am meisten gestört hat.

Eine Rebuild-Pipeline überprüft die Übereinstimmung. Wir erstellen die Version aus dem Quellcode, packen den Tarball und stellen sicher, dass er Byte für Byte mit dem Upstream-Tarball übereinstimmt. Wenn dies der Fall ist, wissen wir, dass unser Rebuild kein einziges Byte der vom Upstream bereitgestellten Version verändert hat. Das ist eine wertvolle Garantie, und wir werden sie gegenüber jedem verteidigen.

Es handelt sich auch nicht um die Garantie, die man gemeinhin annimmt. „Fidelity“ bedeutet, dass wir den Upstream originalgetreu reproduziert haben. Wenn im Manifest des Upstreams steht, dass etwas aus einem persönlichen Fork abgerufen werden soll, sagt ein originalgetreuer Neuaufbau genau dasselbe. Ein originalgetreu reproduziertes Loch ist immer noch ein Loch.

Die Provenienz hat dieselbe Abgrenzung. Eine SLSA-Bescheinigung gibt Auskunft darüber, wie ein Artefakt erstellt wurde, wo und aus welcher Quelle. Sie gibt jedoch keinen Aufschluss darüber, was dieses Artefakt später – Wochen nach Abschluss des Builds – auf einem anderen Rechner vom Paketmanager eines Kunden zum Herunterladen anfordern wird. Das ist der Umfang, den SLSA für die Provenienz definiert, und es ist ein sinnvoller Umfang. Die Provenienz bescheinigt, wie etwas hergestellt wurde, niemals aber, ob es hätte hergestellt werden sollen.

Man könnte stattdessen während der Builds das Netzwerk überwachen, und bei manchen Rebuild-Projekten wird genau das getan. Dabei werden Dinge entdeckt, die ein Manifest-Scan niemals aufdecken würde, wie zum Beispiel ein postinstall Ein Skript, das eine Binärdatei von irgendwoher herunterlädt. Es beschreibt jedoch nur Ihre Build-Umgebung. Die eigentliche Installation findet erst später auf dem Rechner eines anderen statt, und Ihr Build-Proxy bekommt davon nichts mit. Jeder Ansatz deckt einen blinden Fleck des anderen ab.

Die Lehre, die wir daraus gezogen haben, ist, dass Korrektheit und Konsistenz zwei verschiedene Aspekte sind, die jeweils unterschiedliche Prüfungen erfordern. Wenn man eine Konsistenzprüfung in den Verifizierungsschritt einbindet, lässt sich bei einem Fehler nicht mehr feststellen, welcher Aspekt fehlerhaft ist.

Die Korrektur, die die Lage fast noch verschlimmert hätte

Die naheliegende Lösung besteht darin, es zu spiegeln. Kopieren Sie die Git-Abhängigkeit als ordentliches Paket in die Registry, passen Sie die Spezifikation an einen normalen Versionsbereich an und machen Sie weiter.

Es gibt allerdings einen Haken. Sobald man etwas bearbeitet, package.json, stimmt Ihr Tarball nicht mehr mit dem Upstream-Tarball überein, und die Konsistenzprüfung lehnt ihn zu Recht ab. Letztendlich müssen Sie sich zwischen drei gewünschten Zielen entscheiden: Byte-für-Byte-Konsistenz, tatsächliche Inklusion und Pakete, die sich als direkte Ersatzlösungen installieren lassen, ohne dass jemand seinen Code ändern muss. Sie erhalten zwei davon.

Die Benennung der Kopie erwies sich als eigene Falle. In einem Fall verwies die Git-Abhängigkeit auf einen Jahre alten persönlichen Fork, dessen package.json gab einen Paketnamen in Großbuchstaben und eine Version an, die noch nie irgendwo veröffentlicht worden war. npm akzeptiert keine neuen Paketnamen mit Großbuchstaben, daher kam dieser Name nicht in Frage. Der Name in Kleinbuchstaben war in der öffentlichen Registrierung bereits von einem anderen Fork mit einer niedrigeren Versionsnummer belegt. Wenn wir unsere Version unter diesem Namen mit der höheren Versionsnummer veröffentlichen, könnte ein Resolver, der so konfiguriert ist, dass er Kandidaten aus privaten und öffentlichen Quellen vergleicht, unsere Version bevorzugen.

Das entspricht fast dem Lehrbuchbeispiel für Abhängigkeitsverwirrung. Wir hätten genau die Art von Problem geschaffen, die wir eigentlich beheben wollten.

Eine saubere Lösung in einem solchen Fall ist ein neuer Name mit begrenztem Geltungsbereich, der nicht mit öffentlichen Namen kollidieren kann, sowie eine Überarbeitung des Manifests des Verbrauchers, damit es dorthin verweist. Das bedeutet zwar mehr Arbeit, und das Paket folgt dadurch nicht mehr automatisch dem Branch eines anderen. Wir sind jedoch zu der Erkenntnis gelangt, dass dieser letzte Punkt ein Vorteil ist. Das unbemerkte Verfolgen des Standard-Branchs eines Fremden war die Schwachstelle, nicht der Dienst selbst.

Was Sie in Ihren eigenen Projekten überprüfen sollten

Fangen Sie damit an, sie zu finden. npm kann dies mit seinem Abfragebefehl für Sie erledigen:

npm query ":type(git), :type(remote)"

Diese Funktion durchläuft Ihren installierten Abhängigkeitsbaum und gibt alles zurück, was aus einem Git-Repository oder einer Remote-URL stammt, einschließlich transitiver Abhängigkeiten. Wenn dabei etwas gefunden wird, möchten Sie wissen, welche Ergebnisse direkt sind und welche drei Ebenen tiefer liegen.

Falls Sie bereits nach npm 12: Wahrscheinlich hast du einige davon schon auf die harte Tour kennengelernt. Git- und Remote-URL-Abhängigkeiten werden nun standardmäßig blockiert, und eine Installation, bei der eine solche Abhängigkeit auftritt, schlägt fehl, bis du sie mit allow-git oder allow-remote. Das ist eine gute Standardeinstellung – und eine, die schon lange überfällig war. Es ist verlockend, allow-git=all Die Pipeline wird also wieder grün, aber damit bist du wieder genau da, wo du angefangen hast – nur dass jetzt eine Konfigurationszeile vorhanden ist, die den Anschein erweckt, es handele sich um eine bewusste Entscheidung. Wenn sich die einzigen Git-Abhängigkeiten, die du benötigst, in deinem eigenen package.json, das root Der Wert beschränkt die Ausnahme auf diese Fälle und verhindert weiterhin, dass transitive Ausnahmen ausgelöst werden. pnpm 11 bietet von Haus aus fast dieselben Funktionen. Seine blockExoticSubdeps Die Einstellung ist nun standardmäßig auf true, wodurch Git- und Tarball-Spezifikationen in transitiven Abhängigkeiten blockiert werden, während die von dir selbst deklarierten weiterhin zulässig sind.

Beachten Sie jedoch, wo sich diese Steuerung befindet. Es ist der Client, der sich weigert, die Daten abzurufen, und nicht Ihre Registry, die irgendetwas enthält. Ihre private Registry sieht diese Abhängigkeiten nach wie vor nicht. Der Client weigert sich lediglich, sie abzurufen.

Unabhängig davon, was die Abfrage ergibt, sollten Sie sich als Erstes die Struktur ansehen. Ein SHA-Pin in einer Entwicklungsabhängigkeit ist in der Regel ein Containment-Problem mit niedrigerer Priorität. Ein Branch, ein Tag oder eine Bare-Repository-URL in einer Laufzeitabhängigkeit muss behoben werden, entweder durch Umstellung auf eine veröffentlichte Version, sofern vorhanden, oder durch das Einbinden einer festgepinten Kopie unter einem von Ihnen verwalteten, bereichsbezogenen Namen. Und wenn Ihre CI-Runner auf github.com Allerdings stellt sich die Frage, ob dies tatsächlich notwendig ist, da die Ausgangskontrolle diese Abrufe unabhängig vom Inhalt des Manifests erkennt.

Und wo bleibt da der Spiegel?

Das bedeutet jedoch nicht, dass private Registries sinnlos sind. Sie leisten viel Arbeit. Sie beantworten lediglich eine engere Frage als die, die man ihnen stellt. „Stammt dieses Paket aus einer Quelle, die wir kontrollieren?“ ist nicht dasselbe wie „Stammt alles, was dieses Paket benötigt, aus einer Quelle, die wir kontrollieren?“

Diese zweite Frage tauchte immer wieder auf, während wir SecureChain entwickelten, und sie hat unsere Sichtweise auf eine package.json. Wir betrachten nun die Form jeder Abhängigkeitsspezifikation als Sicherheitssignal, genau wie den Versionsbereich.

Falls Ihr Team npm Abhängigkeiten, die sich nicht ohne Weiteres ersetzen lassen – genau dieses Problem versucht TuxCare zu lösen. SecureChain umfasst aktiv gepflegte Pakete, die unter Angabe ihrer Herkunft aus dem Quellcode neu kompiliert wurden, und Endless Lifecycle Support deckt die Frameworks ab, deren Lebenszyklus abgelaufen ist und für die der Upstream-Entwickler keine Patches mehr bereitstellt, auf die Ihre Anwendung jedoch weiterhin angewiesen ist.

Häufig gestellte Fragen

Was bedeutet es, wenn eine Abhängigkeit aus dem Register entweicht?

Das bedeutet, dass ein Paket eine seiner Abhängigkeiten nicht mit einem Namen und einem Versionsbereich, sondern mit einer Git-URL, einer Tarball-URL oder einem anderen Remote-Speicherort angibt. npm löst diese Abhängigkeit direkt anhand des Speicherorts in der Spezifikation auf, anstatt über die in .npmrc, es sei denn, der Client ist so eingestellt, dass er diesen Quelltyp blockiert. In jedem Fall hat eine private Registrierungsstelle oder ein Proxy hierauf keinen Einfluss.

Bietet eine private npm-Registry Schutz vor Git-Abhängigkeiten?

Nein. Das Registry-Routing gilt nur für Abhängigkeiten, die anhand ihres Namens und eines Versionsbereichs deklariert wurden. Daher durchläuft eine Abhängigkeit vom Typ „git“ oder „remote URL“ niemals Ihre private Registry. Was dies verhindern kann, ist die npm der Client selbst. npm 12 blockiert standardmäßig Git- und Remote-URL-Abhängigkeiten über seine allow-git und allow-remote Einstellungen, die unabhängig von der Registrierung verwaltet werden.

Blockiert npm 12 Git-Abhängigkeiten?

Ja, standardmäßig. npm 12 Sätze allow-git und allow-remote zu none, sodass die Installation von Git- und Remote-URL-Abhängigkeiten – einschließlich transitiver Abhängigkeiten – fehlschlägt, sofern Sie dies nicht ausdrücklich zulassen. Die Einstellung allow-git=root erlaubt nur die Git-Abhängigkeiten, die in Ihrer eigenen package.json, während all erlaubt ihre Verwendung an jeder Stelle im Baum.

Ist eine Git-Abhängigkeit, die an einen Commit-SHA gebunden ist, sicherer?

Ja, das ist viel sicherer als ein Verweis auf einen Branch oder ein Tag, da der Eigentümer den Commit-Hash nicht auf anderen Code verweisen kann. Es umgeht jedoch weiterhin Ihre Registry und hängt davon ab, dass das Repository verfügbar bleibt. Behandeln Sie es daher eher als etwas, das Sie im Auge behalten sollten, als als etwas, das Sie ignorieren können.

Bedeutet die SLSA-Herkunft, dass ein Paket sicher installiert werden kann?

Nein. Die SLSA-Provenienz gibt Auskunft darüber, wie und wo ein Artefakt erstellt wurde und aus welcher Quelle es stammt. Sie sagt nichts über den Quellcode selbst aus und deckt auch keine Abhängigkeiten ab, die das Paket bei einer späteren Installation von außerhalb der Registry bezieht.

Zusammenfassung
Ihre private npm-Registry hat eine Schwachstelle | TuxCare
Artikel Name
Ihre private npm-Registry hat eine Schwachstelle | TuxCare
Beschreibung
Git- und URL-Abhängigkeiten umgehen Ihre private npm-Registry vollständig. Wir haben 53.707 Paketversionen untersucht, um festzustellen, wie häufig dies vorkommt.
Autor
Name des Herausgebers
TuxCare
Logo des Herausgebers

Schließen Sie sich 4.500+ Linux- und Open-Source-Experten an!

2x im Monat. Kein Spam.

Inhaltsübersicht
Holen Sie sich die Open-Source-Sicherheitslösungen, die Sie brauchen

Schließen Sie sich 4.500+ Linux- und Open-Source-Experten an!

2x im Monat. Kein Spam.