npm-Sicherheit: Abhängigkeiten und Pakete gegen Angriffe absichern

Lockfiles, Installationsskripte, npm audit, Provenance und Trusted Publishing: Was die Lieferkette von JavaScript-Projekten wirklich schützt
Aktualisiert:
Abstract
- #NPM Sicherheit
- #Node.js Security
- #JavaScript Sicherheitspraktiken
- #Dependency Management
- #Lifecycle-Scripts deaktivieren
- #Granular Access Tokens
- #Provenance Statements
- #Trusted Publishing
- #Open Source Sicherheit
- #Supply-Chain-Attacken
- #Malware in NPM Packages
- #Secure Coding Practices
- #Software Bill of Materials (SBOM)
- #GitHub Dependabot
- #Socket.dev
- #Snyk Security Tools
Ein Befehl, Hunderte fremde Pakete: Wie Sie npm install so einrichten, dass kein Schadcode mitreist
Ein Entwickler fügt einem Projekt eine kleine Bibliothek zum Formatieren von Datumsangaben hinzu. Der Befehl dauert wenige Sekunden, im Ordner node_modules liegen danach aber nicht ein, sondern oft Dutzende neue Pakete von Autorinnen und Autoren, die niemand im Team kennt. Jedes davon darf bei der Installation Code ausführen, und jedes kann beim nächsten Update anders aussehen als heute.
Genau hier setzen Angriffe auf die Software-Lieferkette an. Gemeint sind Angriffe, die nicht Ihre Anwendung direkt treffen, sondern die Bausteine, aus denen sie entsteht: Pakete, Build-Werkzeuge, Zugangsdaten von Maintainern. Welche Einstellungen und Gewohnheiten im npm-Ökosystem tatsächlich schützen und wo ihre Grenzen liegen, zeigen die folgenden Abschnitte. Um Angriffe, die erst im Browser wirken, etwa Cross-Site Scripting oder CSRF, geht es im Beitrag über JavaScript-Sicherheit im Browser.
Warum die npm-Lieferkette ein lohnendes Ziel ist
Die öffentliche npm-Registry ist das größte Paketverzeichnis der Softwarewelt. Ein Paket zu veröffentlichen kostet nichts und erfordert keine Prüfung. Das hat das Ökosystem groß gemacht, bedeutet aber auch: Wer die Zugangsdaten eines Maintainers erbeutet, erreicht mit einer einzigen Veröffentlichung Tausende Projekte, die das Paket direkt oder über Umwege beziehen.
Die Vorfälle der vergangenen Jahre folgen wiederkehrenden Mustern:
- Übernommene Pflege: 2018 übergab der Maintainer von event-stream das Paket an einen Freiwilligen, der darin Code versteckte, der es auf eine bestimmte Bitcoin-Wallet-Software abgesehen hatte.
- Gestohlene Zugangsdaten: Im September 2025 erbeuteten Angreifer über eine Phishing-Mail die Zugangsdaten eines Maintainers und veröffentlichten manipulierte Versionen weit verbreiteter Pakete wie chalk und debug. Wie dieser Fall ablief, beschreibt der Beitrag npm install unter der Lupe.
- Sich selbst verbreitender Schadcode: Kurz darauf tauchte der Wurm Shai-Hulud auf, der gestohlene Tokens nutzte, um weitere Pakete der Betroffenen zu infizieren. Eine zweite Welle folgte im November 2025; Details dazu im Beitrag über den Shai-Hulud-Wurm.
Außerhalb von npm zeigte der Fall XZ Utils 2024, wie geduldig Angreifer vorgehen können: Dort baute eine Person über Jahre Vertrauen als Mitwirkende auf, bevor sie eine Hintertür einschleuste. Gemeinsam ist allen Fällen, dass klassische Schwachstellenscanner sie anfangs nicht erkannten, weil es sich nicht um Programmierfehler handelte, sondern um absichtlich eingeschleusten Code.
Abhängigkeiten festschreiben: Lockfile und npm ci
Der wirksamste Schutz ist zugleich der unspektakulärste: Installieren Sie in jeder Umgebung genau die Versionen, die Ihr Team geprüft hat, und nicht die, die zufällig gerade aktuell sind.
Versionsbereiche und das Lockfile
npm trägt neue Abhängigkeiten standardmäßig mit einem Caret-Bereich wie ^1.2.3 in die package.json ein. Das erlaubt jede spätere Version bis unter 2.0.0. Erscheint morgen eine manipulierte Version 1.6.2, würde eine frische Installation ohne Lockfile sie ziehen.
Das Lockfile (package-lock.json, bei anderen Paketmanagern pnpm-lock.yaml, yarn.lock oder bun.lock) hält dagegen die exakt aufgelöste Version jeder direkten und transitiven Abhängigkeit fest, einschließlich einer Prüfsumme. Transitive Abhängigkeiten sind die Pakete, die Ihre Pakete ihrerseits mitbringen. Das Lockfile gehört ins Git-Repository und wird im Code-Review mitgelesen, zumindest wenn sich dort unerwartet viel ändert.
Wer zusätzlich die direkten Abhängigkeiten ohne Bereich eintragen möchte, setzt in der .npmrc des Projekts:
save-exact=true
Das ist eine Geschmacksfrage. Exakte Versionen in der package.json machen Änderungen sichtbarer, ersetzen aber nicht das Lockfile, denn die transitiven Abhängigkeiten bleiben davon unberührt.
npm ci statt npm install in der Pipeline
In CI/CD-Umgebungen und beim Deployment sollte npm ci laufen. Der Befehl installiert ausschließlich, was im Lockfile steht, löscht vorher node_modules und bricht ab, wenn package.json und Lockfile nicht zusammenpassen. npm install würde das Lockfile dagegen stillschweigend anpassen.
# Reproduzierbare Installation in der Pipeline
npm ci
# Gegenstücke anderer Paketmanager
pnpm install --frozen-lockfile
yarn install --immutable
bun install --frozen-lockfile
Ein verbreiteter Fehler bei Merge-Konflikten: Das Lockfile wird gelöscht und neu erzeugt. Damit landen auf einen Schlag alle Abhängigkeiten auf dem neuesten erlaubten Stand, ungeprüft. Besser ist es, den Konflikt zugunsten eines Zweigs aufzulösen und danach npm install laufen zu lassen, damit npm nur die tatsächlich geänderten Einträge nachträgt.
Transitive Versionen gezielt überschreiben
Steckt eine bekannte Lücke in einem Paket tief im Abhängigkeitsbaum, hilft das Feld overrides in der package.json. Es erzwingt eine bestimmte Version, egal welches Paket sie anfordert:
{
"dependencies": {
"library-a": "^3.0.0"
},
"overrides": {
"lodash": "4.17.21"
}
}
Yarn nutzt dafür das Feld resolutions, pnpm eigene overrides in seiner Konfiguration. Overrides sind ein Pflaster, keine Dauerlösung: Notieren Sie, warum ein Eintrag existiert, und entfernen Sie ihn, sobald das übergeordnete Paket nachgezogen hat.
Installationsskripte: der direkte Weg ins System
Pakete dürfen in ihrer package.json sogenannte Lifecycle-Skripte wie preinstall, install oder postinstall festlegen. npm führt sie bis Version 11 bei der Installation automatisch aus, mit den Rechten des Benutzers, der den Befehl startet. Auf einem Entwicklerrechner bedeutet das Zugriff auf SSH-Schlüssel, Browserprofile und Cloud-Zugangsdaten, in einer Pipeline Zugriff auf deren Secrets. Mehrere der genannten Vorfälle nutzten genau diesen Weg.
Legitim sind solche Skripte vor allem bei Paketen, die nativen Code kompilieren oder Binärdateien herunterladen. Für die große Mehrheit der Pakete sind sie überflüssig. Daher lohnt es sich, sie grundsätzlich abzuschalten und gezielt freizugeben:
# Skripte von Abhängigkeiten global abschalten
npm config set ignore-scripts true
# In der Pipeline: nur Produktionsabhängigkeiten, keine Skripte
npm ci --omit=dev --ignore-scripts
Beachten Sie, dass ignore-scripts bei npm auch die Skripte Ihres eigenen Projekts betrifft, die automatisch vor oder nach Befehlen laufen. Explizite Aufrufe wie npm run build funktionieren weiterhin. Benötigt ein Paket sein Skript wirklich, führen Sie es danach gezielt mit npm rebuild <paketname> --ignore-scripts=false aus, denn auch npm rebuild beachtet die Einstellung.
Neuere Versionen der Paketmanager sind hier strenger voreingestellt. npm blockiert seit Version 12 Skripte von Abhängigkeiten, solange sie nicht im Feld allowScripts der package.json freigegeben sind (npm install-scripts approve <paketname>); die mit Node.js 24 und 26 ausgelieferte npm-Version 11 verhält sich noch wie oben beschrieben. pnpm führt seit Version 10 Skripte von Abhängigkeiten nur noch aus, wenn sie in einer Freigabeliste stehen, Yarn führt sie seit Version 4.14 standardmäßig gar nicht mehr aus; Bun lässt nur Pakete einer eigenen Liste vertrauenswürdiger Pakete zu, die sich über das Feld trustedDependencies durch eine eigene Liste ersetzen lässt. Für Teams ist das ein Argument, auf aktuelle Versionen umzusteigen. Einen Vergleich der Paketmanager finden Sie im Beitrag über npm und Yarn.
Neue Versionen erst nach einer Wartezeit übernehmen
Die meisten manipulierten Versionen fallen innerhalb von Stunden oder wenigen Tagen auf und werden aus der Registry entfernt. Wer neue Versionen erst nach einer Karenzzeit installiert, entgeht einem großen Teil solcher Angriffe, ohne auf Updates zu verzichten.
pnpm bietet dafür die Einstellung minimumReleaseAge in Minuten, seit Version 11 standardmäßig mit dem Wert 1440:
# pnpm-workspace.yaml
minimumReleaseAge: 1440 # neue Versionen frühestens nach 24 Stunden
Yarn kennt seit Version 4.10 eine vergleichbare Option namens npmMinimalAgeGate, npm seit Version 11.10 die Einstellung min-release-age (in Tagen). Alternativ lässt sich bei npm mit --before ein fester Stichtag angeben, zu dem die Registry ausgewertet wird. Auch Update-Werkzeuge wie Renovate (minimumReleaseAge) und Dependabot (cooldown) können Aktualisierungen bewusst verzögern. Ein bis drei Tage sind ein vernünftiger Ausgangswert. Bei Sicherheitsupdates für bekannte Lücken sollten Sie die Wartezeit gezielt umgehen können, sonst schützt die Regel vor dem einen Risiko und verlängert das andere.
npm audit und seine Grenzen
npm audit gleicht die installierten Versionen mit einer Datenbank bekannter Schwachstellen ab (bei npm die GitHub Advisory Database) und schlägt Updates vor.
# Nur Produktionsabhängigkeiten prüfen
npm audit --omit=dev
# Signaturen und Provenance-Angaben der installierten Pakete prüfen
npm audit signatures
Das ist sinnvoll, aber es beantwortet nur eine Frage: Enthält das Projekt eine Version, für die bereits ein Sicherheitshinweis veröffentlicht wurde? Frisch eingeschleuster Schadcode taucht dort erst auf, wenn ihn jemand gemeldet hat. Hinzu kommt viel Rauschen: Viele Meldungen betreffen Entwicklungswerkzeuge oder Codepfade, die Ihre Anwendung nie erreicht. Teams, die bei jedem Treffer die Pipeline anhalten, gewöhnen sich schnell an, die Warnungen zu ignorieren. Und npm audit fix --force kann größere Versionssprünge auslösen, die etwas kaputt machen.
Eine praktikable Regel: Die Pipeline schlägt bei hohen und kritischen Funden in Produktionsabhängigkeiten fehl (npm audit --omit=dev --audit-level=high), alles andere landet in einem regelmäßigen Update-Termin. Ergänzend analysieren Dienste wie Socket oder Snyk das Verhalten von Paketen, etwa neue Netzwerkzugriffe oder Installationsskripte, und können so auch unbekannten Schadcode auffallen lassen. GitHub bietet mit Dependabot automatische Update-Vorschläge und kann eine Softwarestückliste (SBOM) des Repositorys exportieren, also eine maschinenlesbare Liste aller enthaltenen Komponenten.
Weniger Abhängigkeiten, weniger Angriffsfläche
Jede Abhängigkeit ist ein Vertrauensvorschuss. Seit Node.js viele Grundfunktionen selbst mitbringt, lassen sich manche Pakete ersatzlos streichen: fetch statt eines HTTP-Clients, der eingebaute Test-Runner node:test, node --watch statt nodemon oder --env-file statt dotenv. Welche Bordmittel es gibt und ab welcher Version, zeigt der Beitrag Node.js ohne Ballast.
Bevor ein neues Paket ins Projekt kommt, helfen einige Fragen: Wie viele Maintainer gibt es, und wie aktiv ist das Repository? Bringt das Paket Installationsskripte mit? Wie viele transitive Abhängigkeiten zieht es nach? Bei kleinen Hilfsfunktionen ist es oft schneller, die zehn Zeilen selbst zu schreiben, als ein fremdes Paket dauerhaft zu überwachen. Werkzeuge wie Knip finden außerdem Abhängigkeiten, die im Code gar nicht mehr verwendet werden.
Eine Zusatzebene bietet das Permission Model von Node.js. Mit dem Schalter --permission startet ein Prozess ohne Zugriff auf Dateisystem, Kindprozesse, Worker und native Addons, ab Node.js 25 auch ohne Netzwerkzugriff, bis Sie diese einzeln freigeben:
# Nur Lesen im Projektordner erlauben, Schreiben nur in ./tmp
node --permission --allow-fs-read=./ --allow-fs-write=./tmp index.js
Das schränkt Schaden ein, ersetzt aber keine Prüfung, denn die Freigaben gelten für den ganzen Prozess, einschließlich aller geladenen Pakete. Installationsskripte, die npm selbst ausführt, erfasst es nicht. Deno verfolgt dasselbe Prinzip und verweigert Zugriffe standardmäßig.
Für Maintainer: Pakete sicher veröffentlichen
Wer selbst Pakete veröffentlicht, auch nur interne, trägt Verantwortung für alle, die sie nutzen. Die meisten großen Vorfälle begannen mit einem übernommenen Maintainer-Konto oder einem abgegriffenen Token.
Zwei-Faktor-Authentifizierung mit Sicherheitsschlüssel
Aktivieren Sie die Zwei-Faktor-Authentifizierung (2FA) für Anmeldung und Veröffentlichung:
npm profile enable-2fa auth-and-writes
Am besten verwenden Sie dafür einen Sicherheitsschlüssel oder Passkey nach dem WebAuthn-Standard. Einmalcodes aus einer App lassen sich auf einer gut gemachten Phishing-Seite abgreifen und sofort weiterverwenden, ein Hardware-Schlüssel prüft dagegen die Adresse der Seite. In den Paketeinstellungen auf npmjs.com können Sie zusätzlich verlangen, dass jede Veröffentlichung 2FA erfordert. In Organisationen sollte 2FA für alle Mitglieder Pflicht sein, und Teams sollten nur die Rechte erhalten, die sie brauchen.
Tokens: kurzlebig und eng begrenzt
Wo noch Tokens nötig sind, nutzen Sie granulare Access Tokens: beschränkt auf einzelne Pakete oder Scopes, mit Ablaufdatum, möglichst nur lesend und bei Bedarf auf IP-Bereiche begrenzt. Ein Token pro Zweck mit sprechendem Namen erleichtert es, im Ernstfall genau eines zu widerrufen. Nach den Vorfällen 2025 hat GitHub im Dezember 2025 alle klassischen npm-Tokens widerrufen und die Laufzeit schreibender granularer Tokens auf höchstens 90 Tage begrenzt. Prüfen Sie daher, ob Ihre Pipelines noch auf langlebigen Tokens beruhen.
Trusted Publishing und Provenance
Der bessere Weg für automatisierte Veröffentlichungen ist Trusted Publishing. Dabei hinterlegen Sie auf npmjs.com, aus welchem Repository und welchem Workflow ein Paket veröffentlicht werden darf. Die CI-Umgebung, etwa GitHub Actions oder GitLab CI/CD, weist sich dann per OpenID Connect (OIDC) mit einem kurzlebigen Nachweis aus. Ein dauerhaft gespeichertes npm-Token gibt es nicht mehr, und damit auch nichts, das ein Wurm aus einer Konfigurationsdatei stehlen könnte.
# Ausschnitt eines GitHub-Actions-Workflows für Trusted Publishing
permissions:
contents: read
id-token: write # erlaubt den OIDC-Nachweis gegenüber npm
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm publish
Bei Trusted Publishing über GitHub Actions oder GitLab CI/CD erzeugt npm für öffentliche Pakete aus öffentlichen Repositories automatisch eine Provenance-Angabe. Sie belegt öffentlich, aus welchem Quellcode-Stand und welchem Build-Lauf eine Version stammt. Wer ein Paket nutzt, sieht auf der npm-Seite, ob diese Angabe vorhanden ist, und kann sie mit npm audit signatures prüfen. Ohne Trusted Publishing lässt sich Provenance in unterstützten CI-Umgebungen mit npm publish --provenance erzeugen. Eine Garantie für harmlosen Code ist das nicht: Provenance zeigt, woher ein Paket kommt, nicht, ob der Code im Repository sauber war.
Nur veröffentlichen, was hineingehört
Mit dem Feld files in der package.json legen Sie fest, welche Dateien im Paket landen. Das verhindert, dass versehentlich Testdaten, Konfigurationsdateien oder gar eine .env mit Zugangsdaten veröffentlicht werden:
{
"name": "beispiel-paket",
"version": "1.0.0",
"main": "dist/index.js",
"files": ["dist"]
}
Vor jeder Veröffentlichung zeigt npm pack --dry-run, welche Dateien tatsächlich ins Paket gehen. README, LICENSE und package.json nimmt npm unabhängig vom Feld files immer mit.
Private Registries und interne Paketnamen
Viele Unternehmen betreiben eine eigene Registry, etwa mit Verdaccio, GitHub Packages, JFrog Artifactory oder Sonatype Nexus. Sie dient als Zwischenspeicher für öffentliche Pakete und als Ablage für interne. Richtig konfiguriert lassen sich dort Regeln durchsetzen, zum Beispiel eine Wartezeit für neue Versionen oder eine Sperrliste.
Eine Gefahr dabei ist die sogenannte Dependency Confusion: Angreifer veröffentlichen in der öffentlichen Registry ein Paket mit demselben Namen wie ein internes, aber höherer Versionsnummer, und ein falsch eingestellter Client holt sich die öffentliche Variante. Dagegen helfen ein eigener Scope für alle internen Pakete (etwa @firma/…) und eine feste Zuordnung in der .npmrc:
@firma:registry=https://npm.firma.example/
Angenommen, ein Mittelständler entwickelt mehrere interne Webanwendungen mit gemeinsamen Komponenten. Liegen diese Komponenten unter einem eigenen Scope, der ausschließlich auf die interne Registry zeigt, kann ein gleichnamiges öffentliches Paket gar nicht erst installiert werden. Zusätzlich sollte der Scope auf npmjs.com für das Unternehmen registriert sein, damit niemand anderes ihn belegt.
Was das für Ihr Team bedeutet
npm-Sicherheit ist weniger eine Frage teurer Werkzeuge als konsequenter Voreinstellungen. Lockfile ins Repository, npm ci in der Pipeline, Installationsskripte standardmäßig aus, eine kurze Wartezeit für neue Versionen: Diese vier Punkte kosten einen Nachmittag und hätten die Folgen mehrerer Vorfälle von 2025 deutlich begrenzt. Für Maintainer kommen Sicherheitsschlüssel und Trusted Publishing hinzu. Wer heute noch ein langlebiges Publish-Token in den CI-Secrets liegen hat, sollte das zuerst ändern.
Überschätzt wird dagegen npm audit. Es ist ein nützlicher Bestandscheck, aber kein Schutz gegen neuen Schadcode. Unterschätzt wird die Zahl der Abhängigkeiten selbst: Jedes Paket, das Sie nicht brauchen, ist ein Risiko weniger und ein Update weniger. Die größte Lücke bleibt organisatorisch, denn viele verbreitete Pakete werden von Freiwilligen in ihrer Freizeit gepflegt. Unternehmen, die stark auf solche Projekte angewiesen sind, können sie über Programme wie GitHub Sponsors oder Open Collective finanziell unterstützen.
Wie sich solche Prüfungen dauerhaft in Build- und Release-Prozesse einbauen lassen, ohne die Entwicklung auszubremsen, ist Thema unserer Beratung und Seminare zu DevSecOps.
Häufige Fragen zu npm-Sicherheit
Funktionieren Builds noch, wenn Installationsskripte abgeschaltet sind?
In den meisten Projekten ja. Probleme machen vor allem Pakete mit nativem Code oder solche, die beim Installieren eine Binärdatei herunterladen, etwa für Bildverarbeitung oder Browser-Automatisierung. Testen Sie die Umstellung in einem eigenen Zweig, notieren Sie, welche Pakete ihr Skript wirklich brauchen, und geben Sie nur diese frei.
Reicht es, regelmäßig npm audit laufen zu lassen?
Nein. npm audit meldet nur Versionen, zu denen bereits ein Sicherheitshinweis vorliegt. Gegen frisch kompromittierte Pakete helfen ein festgeschriebenes Lockfile, abgeschaltete Installationsskripte, eine Wartezeit für neue Versionen und gegebenenfalls ein Dienst, der das Verhalten von Paketen analysiert.
Woran lässt sich vor der Installation erkennen, ob ein Paket vertrauenswürdig ist?
Eine Garantie gibt es nicht, aber Indizien: eine Provenance-Angabe auf der npm-Seite, ein aktives Repository mit mehreren Maintainern, keine unerwarteten Installationsskripte und überschaubar viele Abhängigkeiten. Hohe Downloadzahlen sind kein Qualitätsmerkmal, bedeuten aber, dass Auffälligkeiten schneller bemerkt werden. Bei kleinen Paketen lohnt ein Blick in den Quellcode.
Was ist zu tun, wenn ein kompromittiertes Paket installiert wurde?
Gehen Sie davon aus, dass alle Zugangsdaten auf dem betroffenen Rechner oder in der betroffenen Pipeline abgeflossen sind. Widerrufen und erneuern Sie Tokens, SSH-Schlüssel und Cloud-Zugänge, setzen Sie das Lockfile auf eine saubere Version zurück und prüfen Sie, ob von Ihren Konten aus Pakete oder Repositories verändert wurden.
Kategorien