Warum ich jede npm-Dependency pinne: eine kleine Gewohnheit, die ein kompromittiertes Paket übersteht

Eine kurze Reflexion darüber, warum sich der Mehraufwand beim Upgrade lohnt, wenn du npm-Dependencies pinnst – und wie eine kleine CI-Guardrail dafür sorgt, dass die Praxis im Team auch Bestand hat.

  • Marco Schäck Marco Schäck
  • date icon

    27. Apr. 2026

Warum ich jede npm-Dependency pinne: eine kleine Gewohnheit, die ein kompromittiertes Paket übersteht

Wenn du JavaScript oder TypeScript schreibst, hast du Dependency-Deklarationen wie ^1.7.2 schon unzählige Male gesehen. Wahrscheinlich sieht es in deiner eigenen package.json gerade genauso aus. Bei mir war das lange genauso.

Es fühlt sich sicher an. Du gibst dir damit Spielraum: Minor-Versionen und Patches laufen automatisch mit, ohne dass du darüber nachdenken musst. Aber diese Art, Dependencies zu deklarieren, tut mehr, als du vielleicht annimmst. Sie macht aus deiner package.json unbemerkt einen Pointer statt einer Spezifikation. Und ein Pointer zeigt nicht immer auf dasselbe Ziel — mit jeder neuen Version ändert sich, worauf er zeigt.

Dieser Beitrag ist eine kurze Reflexion darüber, warum ich dazu übergegangen bin, jede npm-Dependency auf eine exakte Version zu pinnen: was das konkret bedeutet, wo die echten Tradeoffs liegen, und wie eine kleine Open-Source-GitHub-Action, die ich gebaut habe — pin-npm-dependencies — dabei hilft, diese Praxis in unseren Projekten durchzusetzen.

🔍 Was bedeutet “Pinning” eigentlich?

So verstehe ich gepinnt: drei Dinge, die aufeinander aufbauen.

  1. Eine exakte Version in der package.json"axios": "1.7.2", nicht "^1.7.2" oder "~1.7.2".
  2. Ein committetes Lockfilepackage-lock.json, yarn.lock oder pnpm-lock.yaml, eingecheckt in Git. Das friert die aufgelöste Version jeder transitiven Dependency ein, dazu einen Content-Hash für jedes Tarball.
  3. Ein strikter Installer in CI und Dockernpm ci installiert exakt das, was im Lockfile steht, und schlägt fehl, wenn das nicht geht (yarn und pnpm haben Äquivalente). Ein einfaches npm install ist da großzügiger: Fehlt das Lockfile oder passt es nicht zur package.json, löst es einfach neu auf und schreibt das Lockfile still um.

Schritt 2 ist bei den meisten Teams bereits Standard. Schritt 1 erfüllen deutlich weniger — und Schritt 3 fällt häufiger unter den Tisch, als du denkst. Die Integritäts-Hashes im Lockfile schützen dich gegen Manipulation an bereits gelockten Paketen. Aber sie können dich nicht schützen, wenn das Lockfile selbst neu generiert wird — jemand fügt eine Dependency hinzu, ein Renovate-PR wird gemerged, ein CI-Job führt npm install statt npm ci aus. In diesem Moment löst jede Range in der package.json frisch auf, eine neue Version kommt rein, und nichts schlägt Alarm. Genau hier setzt Schritt 1 an und schließt diese Lücke.

⚠️ Warum lockere Versionen ein Supply-Chain-Risiko sind

Diese Unberechenbarkeit ist genau die Lücke, die ein Angreifer braucht. Jede Range in deiner package.json ist eine offene Einladung, beim nächsten npm install einfach das zu nehmen, was gerade aktuell in 1.x verfügbar ist — ohne Prüfung, ohne dass es jemand bemerkt.

Was durch diese Lücke kommt, folgt fast immer demselben ernüchternden Muster: Die npm-Credentials eines Maintainers werden gephisht, oder ein populäres Paket bekommt unauffällig einen neuen Maintainer. Manchmal wird auch ein aufgegebener Paketname einfach neu registriert. Dann erscheint eine bösartige Version. Oft mit einem postinstall-Hook — einem Skript, das npm automatisch direkt nach dem Download ausführt, bevor irgendein Stück deines Codes das Paket je importiert.

Ein eingechecktes Lockfile verschafft dir einen zeitlichen Puffer. Du ziehst die neue Version nicht bei jeder Installation rein. Aber das nächste Mal, wenn das Lockfile neu generiert wird — jemand fügt eine Dependency hinzu, ein Renovate-PR wird gemerged, ein CI-Job führt npm install statt npm ci aus — löst die Range frisch auf, und die neue Version fließt rein. Es gibt keinen PR, der diese Dependency betrifft, kein echtes Review eines 200-Zeilen-Lockfile-Diffs — niemand wirft noch einen zweiten Blick darauf.

Das ist nicht theoretisch. Im März 2026 wurde axios — der meistinstallierte JavaScript-HTTP-Client der Welt — von einem staatsnahen Threat Actor kompromittiert. Die Angreifer haben den axios-Quellcode nicht einmal direkt verändert. Sie nutzten einen gekaperten Maintainer-Account, um zwei neue Versionen zu veröffentlichen, die im Verborgenen eine vorgelagerte bösartige Dependency hinzufügten, deren postinstall-Hook einen plattformübergreifenden Remote-Access-Trojaner ablegte. Das Post-Mortem der axios-Maintainer und Microsofts Threat-Intel-Analyse gehen die ganze Kette durch.

Die bösartigen Versionen waren nur wenige Stunden online — aber wer das Pech hatte, dass sein Lockfile genau in diesem Zeitfenster neu generiert wurde, hat sie automatisch mit installiert, vermutlich ohne überhaupt zu bemerken, dass sich die axios-Version geändert hatte.

Und axios ist nur der jüngste Eintrag auf einer Liste, die immer länger wird — event-stream im Jahr 2018, ua-parser-js 2021, und Dutzende mehr. Die Motive unterscheiden sich, die Angriffsfläche bleibt dieselbe.

🤖 Und dann ist da noch der AI-Aspekt

In den letzten zwei Jahren — vielleicht sogar erst in den letzten Monaten — hat sich diese Diskussion spürbar verändert. AI-Coding-Agenten wie Copilot, Cursor und Claude Code schlagen nicht mehr nur gelegentlich ein Autocomplete vor. Bei vielen Teams schreiben sie inzwischen einen erheblichen Teil der Codebasis: ganze Features, Refactorings, Dependency-Entscheidungen, Install-Befehle, CI-Workflows.

Wie groß ist dieser Anteil? Groß genug, dass Spotify kürzlich öffentlich gemacht hat, dass ihre stärksten Engineers seit Dezember keine Zeile Code mehr getippt haben. Sie shippen mit Agenten. Spotify ist damit sicher ein Extremfall. Aber die Richtung ist überall dieselbe.

Daraus folgen zwei Dinge, die Pinning wichtiger machen — nicht weniger wichtig.

Erstens erben Agenten die Defaults des Ecosystems, auf dem sie trainiert wurden. Das bedeutet: Sie schreiben ohne zu zögern "react": "^18.3.0" hin, solange dein Tooling nicht gegensteuert. Bei Hunderten kleinen Änderungen durch Agenten wächst der ungepinnte Anteil in deinem Repo schleichend.

Zweitens fällt in agentischen Flows der menschliche Review-Schritt oft weg. Wenn ein Agent autonom Dependencies installiert und Tests ausführt, fehlt der Reflex, kurz innezuhalten und zu fragen: “Moment, warum fügen wir diese Dependency überhaupt hinzu?” — ein Reflex, den ein menschlicher Reviewer manchmal hat. Die Zahl der package.json-Entscheidungen, die niemand bewusst getroffen hat, steigt weiter.

Eine CI-Guardrail, die exakte Versionen erzwingt, ist einer der billigsten Wege, die Kontrolle zu behalten, während sich der Rest des Workflows beschleunigt.

⚖️ Der ehrliche Tradeoff: Pinning ist nicht gratis

Pinning ist nicht umsonst. Wäre es das, hätte npm es längst zum Standard gemacht.

Wenn du jede Dependency pinnst, tauschst du impliziten Drift gegen expliziten Aufwand:

  • Jeder Patch-Release wird zu einem kleinen PR — Dependabot oder Renovate funktionieren weiter, sie produzieren nur mehr Rauschen.
  • Lockfile-Konflikte werden häufiger, wenn zwei Branches gleichzeitig Versionen bumpen.
  • Wenn dein Team diese PRs nicht wirklich anschaut, hängst du bei Security-Patches hinterher und stehst am Ende schlechter da als vorher.

Pinning ist ein Constraint, den du dir bewusst auferlegst. Es zahlt sich nur aus, wenn du es mit echter Update-Disziplin paarst — automatisierten Upgrade-PRs, einem festen Zeitfenster, um sie zu reviewen, und npm ci überall, wo es zählt. Ohne das frierst du lediglich den aktuellen Stand der Sicherheitslücken in deinem Repo ein.

Was du im Gegenzug bekommst, ist konkret: Jede Dependency-Änderung taucht als reviewbarer Diff auf. Ein Upgrade wird zu einer bewussten Entscheidung. Die Chance, etwas Seltsames zu erkennen, bevor es in Production landet, steigt von praktisch null auf spürbar mehr.

🛡️ Mach einen Check daraus, keine Konvention

All das zu wissen und ein Team dazu zu bringen, sich daran zu halten, sind zwei verschiedene Probleme. Konventionen in einer README verblassen mit der Zeit; ein einziges npm install <pkg> ohne --save-exact schreibt wieder einen Caret hinein. Wenn dir Pinning wichtig ist, lohnt es sich, es so zu behandeln wie jede andere Invariante in deinem Code: als etwas, das die CI bei jedem PR prüft — nicht als etwas, das sich jeder Entwickler merken muss.

Dieser Check lässt sich einfach umsetzen — den Build fehlschlagen lassen, sobald irgendeine package.json eine nicht-exakte Version enthält, mutable Git-Refs wie #master eingeschlossen. Du kannst dir das mit ein paar Zeilen Skript selbst bauen, oder die kleine Open-Source-GitHub-Action nehmen, die ich genau dafür gebaut habe: pin-npm-dependencies. Sie funktioniert mit npm, yarn und pnpm. Kombiniere sie mit save-exact=true in deiner .npmrc, damit npm install <pkg> lokal exakte Versionen schreibt — dann hast du beide Seiten abgedeckt.

Der Punkt ist nicht das spezifische Tool. Es ist, dass Pinning nur dann von Dauer ist, wenn etwas anderes als Disziplin es durchsetzt.

🤝 Wie siehst du das?

Hat dir schon mal eine transitive Dependency ungefragt den Boden unter den Füßen weggezogen, oder hat dein Team Frieden mit Caret-Ranges geschlossen? Mich würde interessieren, wo für dich in deinem Kontext die Linie richtig sitzt.

This post was originally published in English on Medium.com.

Fandest du den Artikel interessant?

Abonniere unseren Newsletter und erhalte neue Beiträge direkt ins Postfach.

Insights

Aktuelle Beiträge

Die neuesten Artikel aus unserem Blog: technische Deep Dives, Erfahrungsberichte und Einblicke in unsere Arbeit.

Agent-Skills: Wie AI auch in Nischen-Domänen wie BPMN & Prozessautomatisierung zuverlässig wird
date icon

18. Juni 2026 · Marco Schäck

Agent-Skills: Wie AI auch in Nischen-Domänen wie BPMN & Prozessautomatisierung zuverlässig wird

AI macht Routine-Coding austauschbar – doch in Nischen-Domänen wie der Prozessautomatisierung mit BPMN und Zeebe liefern reine Prompts inkonsistente Ergebnisse. Wie Agent-Skills Domänenwissen kodieren und AI auch hier zuverlässig machen.

Weiterlesen
Beyond the Happy Path: Was eine Camunda-8-Migration wirklich schwer macht
date icon

01. Juni 2026 · Thomas Heinrichs

Beyond the Happy Path: Was eine Camunda-8-Migration wirklich schwer macht

Warum der Wechsel auf Camunda 8 kein technisches Upgrade ist, sondern eine Plattform-Transformation: Erkenntnisse aus unserem CamundaCon-2026-Vortrag in Amsterdam.

Weiterlesen
Miragon AI: Vom Cockpit zur Konversation
date icon

04. Mai 2026 · Thomas Heinrichs

Miragon AI: Vom Cockpit zur Konversation

Wie wir bei Miragon Prozessautomatisierung neu denken: weg vom isolierten Cockpit, hin zur Konversation als Interface. Warum das MCP und MCP Apps das Fundament dafür bilden.

Weiterlesen

Innerhalb eines Tages weißt du, wo du stehst.

Kein Verkaufsgespräch, sondern eine ehrliche Einschätzung, ob und wie Automatisierung dir hilft.

Erstgespräch vereinbaren

Kein Commitment. Kein Pitch-Deck. Nur Klarheit.