npm Lockfile CVE Remediation (Deutsch)
Status: Entwurf
Sprache: Deutsch
Companion: English
Ein werkzeugunabhängiges Verfahren, um Lockfile-CVEs in einem npm-Projekt zu beheben: package-lock.json so korrigieren, dass bekannt verwundbare Versionen nicht mehr aufgelöst werden, ohne den Anwendungsquellcode anzufassen und ohne die Installation stillschweigend zu brechen.
Es ist eine operative Ergänzung zum Agentic Coding Playbook, nützlich immer dann, wenn ein Agent oder ein Mensch Dependency-CVEs in einem lockfile-basierten npm-Repository behebt.
Zweck
Die meisten CVEs gegen ein Node-Projekt sind Lockfile- oder transitive Probleme, keine Anwendungscode-Probleme: Der Fix besteht darin, zu ändern, was package-lock.json auflöst, nicht den Quellcode zu ändern. Das klingt mechanisch, aber drei Fehlermodi machen es leicht, ein Lockfile auszuliefern, das repariert aussieht und tatsächlich kaputt ist:
- ein Fix, der nach npms eigener Audit-Prüfung stimmig ist, aber von
npm ciauf der CI-Toolchain nicht installierbar ist; - ein Fix, der stillschweigend einen verschachtelten Lockfile-Eintrag entfernt, den eine Peer-Dependency noch braucht;
- ein Override, der den Advisory schließt, aber einen Consumer zur Laufzeit unsichtbar bricht, weil kein Test die betroffene Codepfad-Stelle ausführt.
Dieses Verfahren soll alle drei erkennen, bevor die Änderung ins Review geht.
Wann anwenden
Anwenden, wann immer ein Scanner (Dependabot, npm audit oder ein Äquivalent) eine Schwachstelle meldet, deren gefixte Version innerhalb der aktuellen Major-Version des betroffenen Pakets existiert, oder wann immer der Fix das Cross-Major-Override-Rezept weiter unten für ein gekoppeltes Paketpaar braucht.
Nicht anwenden auf einen Fix, der einen breaking Major-Bump eines Pakets erfordert, das tatsächlich in der Anwendungslogik verwendet wird; das ist eine separate, urteilsintensive Änderung, die ins normale Upgrade-Review gehört statt in diesen Ablauf.
Verfahren
1. Erst fetchen, dann zählen
npm audit liest das ausgecheckte Lockfile, nicht den entfernten HEAD. Ein Checkout, der auch nur wenige Commits hinter dem Default-Branch liegt, meldet zu wenig oder zu viele Treffer. git fetch ausführen und bestätigen, dass gegen den aktuellen Default-Branch gezählt wird, bevor irgendeine Zahl als Baseline behandelt wird.
2. Die CI-npm-Version bestimmen und für das gesamte Verfahren fixieren
Verschiedene Repositories können unterschiedliche Node-Majors pinnen, die wiederum die gebündelte npm-Major-Version bestimmen. Den Pin lesen, nicht annehmen:
grep -A4 'actions/setup-node' .github/workflows/*.ymlnode-version: aus diesem Step lesen. Wenn der Workflow zusätzlich eine explizite npm-Version pinnt (ein npm install -g npm@<x>-Step, oder ein packageManager-Feld via corepack), gewinnt dieser Pin gegenüber dem Node-gebündelten Default. Andernfalls die Node-gebündelte npm-Version für diese Major-Version auflösen und Node und npm gemeinsam in derselben Shell pinnen, nicht nur nvm use allein. Das Beispiel unten verwendet nvm:
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" # no-op where nvm is already a shell function
nvm install <major> # no-op if already installed locally
nvm use <major> && export PATH="${NVM_BIN:?nvm activation failed}:$PATH"
node --version && npm --versionDie übertragbare Anforderung, unabhängig davon, welchen Versionsmanager ein Repository verwendet: Node und npm gemeinsam in genau der Shell aktivieren, die den Fix und die Verifikation ausführt, und beide anschließend mit node -v und npm -v in derselben Shell bestätigen.
Das "switched to"-Banner eines Versionsmanagers ist kein Beleg dafür, was die Shell tatsächlich auflöst: Ein früherer PATH-Eintrag kann das Shim des Switchers überdecken, sodass das Banner die richtige Version anzeigt, während node/npm trotzdem woanders aufgelöst werden. npm -v selbst in genau der Shell ausgeben, die den Fix und die Verifikation ausführt, und einen Treffer gegen die CI-Version als vor dem Lauf geprüfte Tatsache behandeln, nicht als Annahme, die vom Switch-Befehl übernommen wird. Stimmt der Stempel nicht überein, die vollständige Sourcing-plus-export PATH-Form oben in genau dieser Shell erneut ausführen; stimmt er weiterhin nicht überein, prüfen, ob der Workflow npm explizit pinnt (corepack oder npm install -g npm@<x>) und diese gepinnte Version direkt installieren, statt die Versionsmanager-Form erneut zu versuchen. Ein blanker absoluter Pfad zu einer npm-Binary ist kein Ersatz: Viele npm-Launcher-Skripte tragen ein #!/usr/bin/env node-Shebang, sodass ein absoluter npm-Pfad nur npm pinnt, während Node weiterhin aus dem umgebenden PATH aufgelöst wird, was zu einer Node/npm-Paarung führt, die CI nie ausführt.
Dieser Pin muss für jeden Schreib- und Verifikationsschritt unten in derselben Shell aktiv bleiben: Die Aktivierung überträgt sich nicht zwischen separaten Shell-Aufrufen, also in jeder neuen Shell erneut sourcen und aktivieren, statt sich darauf zu verlassen, dass eine frühere Aktivierung noch wirkt.
3. Das Lockfile fixen, innerhalb der Major-Version bleiben
npm audit fix --package-lock-only, unter der gepinnten CI-npm-Version, behebt die meisten Advisories innerhalb der Major-Version.- Für eine direkte Dependency, die der Fixer zurücklässt (oft konservativ gepinnt), diese explizit anheben, unter der gepinnten CI-npm-Version:
npm install <pkg>@<fixedVersion> --package-lock-only -w <workspace>(-wweglassen bei einem Single-Package-Repository). - Vor dem Weitermachen mit dem Kohärenz-Gate unten verifizieren.
Hier nur Bumps innerhalb der Major-Version anwenden. Ein Fix, der einen breaking Major-Bump braucht (typischerweise Dev-, Build- oder Test-Tooling), ist außerhalb des Scopes: als separates, explizit reviewtes Upgrade zurückstellen, statt es in einen CVE-Fix zu falten. Audit-Vorschläge ignorieren, die tatsächlich ein unabhängiges Paket downgraden würden; das sind False Positives aus der Constraint-Suche des Resolvers. Nie den Anwendungsquellcode anfassen, um eine transitive CVE zu beheben.
Lockfile-Writer-Major: eine ältere npm-Version keine unbeteiligten Einträge umschreiben lassen
Ein Lockfile, das eine neuere npm-Major-Version geschrieben hat und das eine ältere danach umschreibt (auch durch ein No-op-npm install --package-lock-only), kann Felder verlieren, die die ältere Version nicht schreibt. Das beobachtete Symptom: Die libc-Arrays an optionalen nativen Binary-Einträgen (zum Beispiel lightningcss-* oder glibc/musl-Varianten von @rollup/rollup-linux-*) verschwinden aus Einträgen, die der Fix gar nicht anfassen sollte. Das begräbt die eigentliche Änderung in unbeteiligtem Churn und zwingt den Reviewer, Einträge neu zu prüfen, die sich nicht hätten ändern müssen. Die Ursache ist die Writer-Major-Version, nicht das Advisory. Reproduziert wurde es mit npm 11 als Writer und npm 10 als Umschreiber; die ältere Version entfernt die Arrays.
Regel: Die npm-Major-Version, die das Lockfile zuletzt geschrieben hat, schreibt es auch weiter. Das pro Repository festlegen (die CI-npm-Version aus Schritt 2 oder die Major-Version, die das committete Lockfile geschrieben hat) und festhalten, zum Beispiel in einem packageManager-Feld. Ist die CI-npm-Version älter als der Writer des committeten Lockfiles, sie die Datei nicht umschreiben lassen, sondern die Änderung von Hand einpflegen. Das ist die einzige Ausnahme von den Punkten 1 und 2 aus Schritt 3; das Kohärenz-Gate in Schritt 4 läuft weiterhin unter der CI-npm-Version.
Rezept zum Einpflegen von Hand, für jeden Zieleintrag:
- Nur den Zieleintrag
node_modules/<pkg>inpackage-lock.jsonbearbeiten (version,resolved,integrity,dependenciesundoptionalDependencies, soweit vorhanden), dazu den passenden Bereich in der zuständigenpackage.jsonund deren Manifest-Eintrag im Lockfile, wenn das Paket eine direkte Dependency ist:packages[""]für die Root oderpackages["<workspace-path>"]für einen Workspace. - Die Registry-Metadaten abrufen, nie aus dem Gedächtnis oder einem benachbarten Eintrag übernehmen:
npm view <pkg>@<version> version dist.tarball dist.integrity dependencies optionalDependencies --json. - Für jeden von Hand eingepflegten Eintrag alle drei Prüfungen unten ausführen. Das allgemeine Dry-Run-Gate in Schritt 4 gilt zusätzlich und ersetzt diese Prüfungen nicht.
version,resolved(gegendist.tarball),integrity(gegendist.integrity),dependenciesundoptionalDependenciesmit den Metadaten ausnpm view <pkg>@<version>vergleichen, einschließlich fehlender Dependency-Felder. Jede Abweichung vor dem Fortfahren untersuchen.- In einem sauberen Checkout mit der CI-npm-Version aus Schritt 2 ein echtes
npm ci --cache "$(mktemp -d)"(nicht--dry-run) ausführen und den ungepipten Exit-Code erfassen; er muss 0 sein. Der inhaltsadressierte npm-Cache kann eine Installation mit warmem Cache ohne erneuten Tarball-Abruf bedienen und einen falschen Integrity-Wert verdecken. Ein leerer Cache erzwingt die Integrity-Prüfung anhand heruntergeladener Inhalte.npm cischreibt das Lockfile nie; ein danach unverändertes Lockfile beweist daher nichts. Über optionale Einträge sagt der Exit-Code nichts: npm überspringt ein optionales Paket, dessen Tarball die Integrity-Prüfung nicht besteht (beobachtet bei den@next/swc-*-Plattform-Binaries), und endet trotzdem mit 0; für diese Einträge ist der Registry-Abgleich oben die einzige Prüfung. - In getrennten sauberen Kopien von Basis und geplantem Head auf beiden Seiten mit npm 10
npm install --package-lock-onlyausführen und dann die normalisiertenpackage-lock.json-Dateien diffen. Der Diff darf nur die beabsichtigten Zieleinträge enthalten (einschließlich des zuständigen Root- oder Workspace-Manifest-Eintrags bei einer direkten Dependency). npm 10 entfernt dielibc-Arrays von npm 11 aus unbeteiligten Einträgen; wenn nur der Head normalisiert wird, erscheinen diese als unbeteiligte Änderungen im Diff. Das von Hand bearbeitete Lockfile bleibt die einzureichende Datei; die Kopien dienen nur zum Vergleich der normalisierten Ergebnisse.
4. Das Kohärenz-Gate: npm ci --dry-run, sauberer Checkout, CI-npm-Version
Dass ein Lockfile bei npm audit --package-lock-only sauber ausgewiesen wird und dass npm install --package-lock-only byte-identisch als No-op reproduziert, ist kein Beleg dafür, dass es installierbar ist. npm audit liest package-lock.json nur auf bekannte Advisories; es installiert nie. Ein No-op-npm install zeigt nur, dass sich nichts über den bereits vorhandenen Stand hinaus ändern würde, nicht dass es kohärent genug ist, damit npm ci es akzeptiert.
Zwei npm-Majors können sich zudem über dasselbe Lockfile uneinig sein: Eine neuere npm-Version kann einen fehlenden verschachtelten Eintrag tolerieren, den eine ältere ablehnt, sodass ein Fix, der lokal sauber installiert, unter der von CI gepinnten npm-Major-Version trotzdem bei npm ci scheitern kann. Dieses Gate speziell unter der CI-npm-Version ausführen, unmittelbar nach dem Lockfile-Fix (Schritt 3) und vor dem Review, für jeden betroffenen Baum (Root plus jeden Workspace oder jedes Sub-Package mit eigenem Lockfile). Ein Baum, der es nicht besteht, geht zurück zur Reparatur, nicht weiter ins Review.
a. Einen sauberen Checkout extrahieren. Ein vorhandenes node_modules in einem bereits bearbeiteten Checkout kann die Diskrepanz verdecken:
SNAP=$(mktemp -d)
git archive <branch-or-sha> -- <tree-subdir> | tar -x -C "$SNAP"
cd "$SNAP/<tree-subdir>" # or "$SNAP" directly when the tree is the repo rootb. Den Dry-Run ausführen, CI-npm-Version aktiv, Skripte deaktiviert:
npm ci --dry-run --no-audit --no-fund --ignore-scripts--ignore-scripts ist wichtig: Ohne diese Option meldet ein fehlendes lokales Dev-Tool, das von einem prepare-Skript referenziert wird, einen unabhängigen Exit-Code, der wie ein Lockfile-Bruch aussieht und keiner ist. Ein echter Bruch sieht so aus:
npm error code EUSAGE
npm error `npm ci` can only install packages when your package.json and
package-lock.json or npm-shrinkwrap.json are in sync. Please update your lock
file with `npm install` before continuing.
npm error Missing: <pkg>@<version> from lock fileDen Exit-Code direkt erfassen, nie hinter einer Pipe: npm ci --dry-run ... | tail -N; echo $? meldet den Exit-Status von tail, nicht den von npm ci, und ein abgeschnittenes, aber bestehendes tail kann als Erfolg gelesen werden, obwohl die Installation fehlgeschlagen ist. Den Befehl blank ausführen und $? direkt danach erfassen, bei Bedarf zunächst in eine Datei umleiten, wenn die Ausgabe paginiert werden muss: LOG=$(mktemp); npm ci --dry-run ... > "$LOG" 2>&1; echo $? (ein frisches mktemp pro Lauf, kein fester Dateiname; siehe "Parallele Reviewer isolieren" unten). Ist eine Pipe wirklich unvermeidbar, set -o pipefail davor setzen, statt einen Exit-Code aus einem Shell-Array zu lesen. Ohne pipefail meldet eine Pipeline den Exit-Status des letzten Befehls, unabhängig davon, was vorher in ihr fehlgeschlagen ist:
set -o pipefail; sh -c "exit 7" | tail -n1; echo $? # reports 7
sh -c "exit 7" | tail -n1; echo $? # reports 0, tail's own statusSich nicht auf ein PIPESTATUS-artiges Array als Ersatz verlassen: Schreibweise und Indexierung sind shell-spezifisch (${PIPESTATUS[0]} in bash, nullbasiert; ${pipestatus[1]} in zsh, kleingeschrieben, einsbasiert), eine Shell ohne beides liest es als leer, und eine numerische Absicherung gegen diesen leeren Wert schlägt fail-open aus. set -o pipefail gegenüber beiden Array-Formen bevorzugen.
c. Attributions-Kontrolle (verpflichtend): dieselbe Prüfung gegen die Baseline vor dem Fix ausführen (derselbe Baum, Default-Branch-HEAD vor dem Fix, dasselbe Rezept aus sauberem Checkout plus npm ci --dry-run, dieselbe CI-npm-Version):
- Baseline OK, Fix-Branch FAIL: der Fix hat es verursacht. Jetzt reparieren, bevor dieser Baum weitergeht.
- Baseline FAIL, Fix-Branch FAIL: vorbestehende Altlast, nicht die dieses Fixes. Loggen; nicht stillschweigend "beheben" oder als durch diese Änderung verursacht durchwinken.
- Nur ein Baseline-OK-mit-Fix-Branch-FAIL-Paar ist diesem Fix zuzuschreiben. Ein roter Lauf allein ist kein Beleg; erst die Baseline prüfen.
d. Reparieren, CI-npm-Version aktiv, im echten Branch (nicht im Wegwerf-Snapshot), dann mit einem frisch extrahierten sauberen Checkout erneut verifizieren:
npm install --package-lock-onlymit der CI-Version-npm-Binary ausgeführt, verschachtelt neu, was eine Peer-Dependency tatsächlich braucht, statt es wegzuhoisten. Die Schritte (a)-(b) gegen den reparierten Branch wiederholen, um zu bestätigen, dass der Dry-Run mit 0 endet, bevor es ins Review geht.
5. Auf entfernte verschachtelte Einträge prüfen
npm audit fix --package-lock-only hoistet Pakete neu, und Hoisting kann stillschweigend einen verschachtelten Eintrag löschen, von dem noch etwas abhängt: Ein Neu-Hoisting eines Pakets auf eine neuere Version an der Root kann eine verschachtelte, ältere Kopie entfernen, die eine abhängige Komponente über einen engeren Semver-Bereich noch braucht, selbst wenn nichts in package.json das sagt, und selbst wenn npm audit das Ergebnis als sauber meldet.
Nach jedem npm audit fix die Package-Keys des Lockfiles gegen die Basis vor dem Fix diffen und auf entfernte verschachtelte Pfade prüfen (jeder Key mit mehr als einem node_modules/-Segment). Alles wiederherstellen, was verschwunden ist und von dem ein anderer Eintrag noch abhängt. Ein verschachtelter Eintrag, den ein früherer Commit absichtlich platziert hat, ist tragend, auch wenn nichts in package.json das erzwingt, und das Kohärenz-Gate oben fängt das nicht immer ab: Ein Workspace, den CI nicht baut oder testet, kann so brechen und trotzdem eine grüne Pipeline zeigen.
6. "Nur Dev" ist eine Behauptung, keine Annahme
Ein Advisory zurückzustellen, weil das Paket nur Dev betrifft, ist ein vertretbarer Entscheid, aber pro Repository verifizieren, statt die Formulierung aus einem früheren Fix zu übernehmen. Das eigene dev-Flag des Lockfiles prüfen und dessen Polarität richtig lesen: npm-Lockfile v2/v3 schreibt dev: true auf einen reinen Dev-Eintrag und lässt den dev-Key bei einem Eintrag der Produktions-Closure komplett weg, es schreibt also nie dev: false. Ein Eintrag ohne dev: true ist Teil der Produktions-Install-Closure, unabhängig davon, was package.json nahelegt. Ein Paket kann indirekt in der Produktions-Closure landen, über eine unabhängige Produktions-Dependency, die das verwundbare Paket unter dependencies deklariert, obwohl sie es zur Laufzeit nie verwendet; das dev-Flag des Lockfiles bestimmt die Exposition.
Mit einem positiven Befehl verifizieren, statt das Flag von Hand zu überfliegen: npm ls --omit=dev <pkg> gibt das Paket aus (Exit 0), wenn es vom Produktionsbaum aus erreichbar ist, und gibt (empty) aus (Exit 1), wenn nicht. Diesen Befehl ausführen, bevor ein Advisory als "nur Dev" zurückgestellt wird.
7. Majors zurückstellen
Ein Fix, der einen Semver-Major-Bump braucht (typischerweise Build-, Lint- oder Test-Tooling, manchmal aber auch eine Laufzeit-Dependency), liegt außerhalb des Scopes eines Lockfile-CVE-Fixes. Als separaten, explizit reviewten Upgrade-Task ablegen, statt ihn hier zu bumpen, auch wenn das Audit-Tool es vorschlägt.
Das Cross-Major-Override-Rezept
Manche Advisories haben keinen In-Major-Fix für eine der Major-Linien des Pakets: Jedes Release auf dieser Linie ist verwundbar, und der Fix existiert nur auf der nächsten Major-Version. Ein konkretes Beispiel ist GHSA-mh99-v99m-4gvg (ein brace-expansion-Regex-Denial-of-Service-Advisory): Seine Fix-Linie ist pro Major (ein gewöhnlicher Bump innerhalb der Major-Version behebt es auf den 1.x/2.x/3.x-Linien), aber die 4.x-Linie hat keinen In-Major-Fix, sie schließt nur durch den Wechsel auf 5.x. Die aktuellen Fix-Linien des Advisorys vor der Anwendung dieses Beispiels erneut prüfen; die Fix-Linien eines Advisorys können nach Veröffentlichung überarbeitet werden. Wenn die nächste Major-Version auch die exportierte API-Form ändert und ein gekoppeltes Paket gegen diese Form neu geschrieben wurde, kann das Schließen des Advisorys erfordern, beide Pakete per overrides auf passende neue Major-Versionen zu zwingen:
"overrides": {
"brace-expansion": "^5.0.8",
"minimatch": "^10.2.5"
}Warum beide Pakete zusammen, als allgemeine Regel. brace-expansion 5.x hat seine Export-Form geändert (ein Objekt benannter Exporte statt der aufrufbaren expand()-Funktion früherer Linien), und minimatch 10 wurde neu geschrieben, um diese Form aufzurufen, und exportiert selbst nun ein Objekt statt der aufrufbaren Funktion, die die meisten früheren Major-Versionen exportierten. Nur eine Hälfte eines gekoppelten Paares zu pinnen bricht zur Laufzeit, nicht zur Installationszeit: npm install/npm ci gelingen in beiden Fällen, sodass das Kohärenz-Gate aus Schritt 4 das nicht abfängt. Als allgemeine Regel: nicht annehmen, dass die sichere Grenze allein aus der Major-Nummer als "neue Major sicher, alles darunter unsicher" folgt, und nicht annehmen, dass der deklarierte Bereich eines Consumers für das betroffene Paket über dessen eigene Patch-Releases hinweg monoton ist: Ein späteres Patch-Release eines Consumers kann auf einen älteren, weiterhin verwundbaren Bereich zurückfallen, selbst nachdem ein früheres Patch zu einem gefixten gewechselt war. Die Fix-Linie selbst ist eine Grenze pro betroffener Major-Version; was variiert, ist der Bereich, den jeder Consumer tatsächlich dagegen deklariert. Die tatsächliche Fix-Linie pro Major-Version des Advisorys und den tatsächlich deklarierten Bereich sowie die exportierte Form des installierten Paares im jeweiligen Repository prüfen, statt eine Schlussfolgerung von anderswo zu übernehmen.
overrides auf oberster Ebene, ohne Package-Path-Qualifizierer, schreibt die Anfrage jedes Consumers nach diesem Paket auf dieselbe Version um, unabhängig davon, welchen Bereich dieser Consumer deklariert hat (ein eslint-Plugin, das nur minimatch ^3.1.2 anfragt, bekommt trotzdem die 10.x des Overrides darunter substituiert). Das macht es zu einer Cross-Major-Zwangssubstitution für Consumer, die vor dem Override glücklich auf einem älteren, kompatiblen Paar verschachtelt waren, genau die Form, die "Majors zurückstellen" oben vermeiden will. Nur darauf zurückgreifen, wenn Verschachtelung allein das Advisory nicht behebt: in der Praxis, wenn ein Consumer tatsächlich auf die vollständig verwundbare Major-Linie ohne In-Major-Ausweg gepinnt ist. Bevorzugt npm audit fix (oder einen einfachen transitiven Bump) die gefixte Version nur unter den Consumern verschachteln lassen, die bereits einen kompatiblen Bereich für die neue API deklarieren, und andere Consumer auf ihrem eigenen kompatiblen älteren, verschachtelten Paar belassen; dieses Ergebnis braucht keine der Prüfungen unten, weil nichts eine Major-Grenze für einen Consumer überschreitet, der dem nie zugestimmt hat.
Vorbedingung 1: auf einen ESM-Default-Export-Bruch prüfen. Wenn ein Consumer einen ESM-Default-Import des betroffenen Pakets macht (zum Beispiel @eslint/eslintrcs Config-Array-Loader mit import minimatch from "minimatch") und die neue Major-Version nur benannte Bindungen exportiert, bricht dieser Consumer, sobald das Modul importiert wird, noch bevor seine eigene Logik läuft. Gegen den eigenen installierten Baum des Repositorys prüfen, bevor das Override irgendwo angewendet wird:
node -e "import('@eslint/eslintrc').then(()=>console.log('ok'),e=>{console.error(e.constructor.name+': '+e.message);process.exit(1)})"Ein Bruch sieht so aus:
SyntaxError: The requested module 'minimatch' does not provide an export named 'default'Ein Consumer, der den betroffenen Importpfad nie lädt (zum Beispiel ein reines Flat-Config-Setup ohne Legacy-Kompatibilitätsschicht), trifft diese Vorbedingung nicht; zuerst prüfen, ob dieser Pfad überhaupt geladen wird.
Vorbedingung 2: auf einen CJS-Callable-Bruch prüfen. Wenn das betroffene Paket früher eine aufrufbare Funktion exportierte und die neue Major-Version ein Objekt exportiert, wirft jeder installierte Consumer, der es weiterhin als Funktion aufruft (eine direkte require('minimatch')(...)-Kette, eine lokale Variable, die aus require zugewiesen und später aufgerufen wird, oder das Babel-Interop-Äquivalent eines Default-Import-Aufrufs), einen "not a function"-Fehler in dem Moment, in dem dieser Codepfad läuft, nicht beim Import. Der Bruch ist latent: Er zeigt sich nur, wenn die konkrete Regel, Option oder Verzweigung, die den Aufruf erreicht, tatsächlich läuft, und er kann sich über mehrere Call-Sites in mehreren eslint-Plugins gleichzeitig erstrecken; jede solche Liste als Untergrenze behandeln, die pro Repository neu abzuleiten ist, nie als abgeschlossene Zählung.
Gegen das eigene node_modules des Repositorys erfassen, nicht gegen eine aus einem früheren Fix übernommene Liste, da Aufrufformen und -zahlen zwischen Paketen und Versionen variieren:
grep -rnE "require\('minimatch'\)\(|\(0, *_minimatch[0-9]*(\.default|\['default'\]|\[\"default\"\])\)\(" node_modules/grep -rl "= require('minimatch')" node_modules/Der erste erfasst verkettete Aufrufe und Babel-Interop-Formen. Der zweite findet Dateien, die den Import einem lokalen Namen zuweisen, der später in derselben Datei aufgerufen werden kann (eine Form, die manche eslint-plugin-react-Regeln verwenden und die im ersten grep nicht auftaucht); dessen Ausgabe als Kandidatenliste zur manuellen Prüfung behandeln, nicht als endgültige Zählung. Beide als Startliste behandeln, die pro Repository neu abzuleiten ist, nicht als einmal abzuhakende Checkliste.
Die Erreichbarkeit gegen die eigene reale Konfiguration des Repositorys bestätigen, da der Bruch nur auftritt, wenn die konkrete Regel/Option/Verzweigung aktiv ist:
npx eslint . --format jsonEin Bruch endet mit einem Exit-Code ungleich null ohne jedes JSON auf stdout; der Lauf bricht ab, statt einen Lint-Fehler zu melden, zum Beispiel:
TypeError: minimatch is not a function
…/eslint-plugin-react/lib/rules/jsx-pascal-case.js:66:34Ein grüner Lauf ist kein Beleg für Sicherheit, nur für Abwesenheit auf heute ausgeführten Pfaden:
SCRATCH=$(mktemp -d); OUT="$SCRATCH/eslint-out.json"
npx eslint . --format json > "$OUT"
node -e "console.log(require(process.argv[1]).length)" "$OUT"Ein leeres $OUT bedeutet, dass der Lauf vor dem Schreiben der Ausgabe abgebrochen ist, nicht dass null Dateien gelintet wurden; den Exit-Code des eslint-Aufrufs selbst prüfen, bevor eine leere oder fehlende Datei als "null Befunde" vertraut wird. Bestätigen, dass der Lauf tatsächlich eine nicht-triviale Anzahl an Dateien gelintet hat, bevor ein Bestehen vertraut wird; ein veraltetes Include/Exclude-Glob kann still null Dateien treffen. Eine heute nicht aktivierte Regel kann denselben kaputten Aufrufpfad morgen erreichen, sobald sie aktiviert wird, oder in einem nicht geprüften Schwester-Repository; ein sauberer Lauf heute sagt über keines der beiden etwas aus.
Die Kopplungsfalle: nicht durch Downgrade des gekoppelten Pakets "lösen". Die gefixte Major-Version eines Pakets mit einer älteren Major-Version seines gekoppelten Pakets zu paaren ist keine kleinere, sicherere Variante dieses Overrides; es ist ein dritter, schlimmerer Bruch, weil die neue Major-Version des ersten Pakets genau die API ist, gegen die die neue Major-Version des zweiten Pakets neu geschrieben wurde. brace-expansion@5.0.8 über ein älteres minimatch@3.1.5 zu zwingen (kein verschachteltes kompatibles Paar, eine einzige flache Installation) bricht so:
const minimatch = require('minimatch');
minimatch('a/x.ts', '**/*.{ts,tsx}');TypeError: expand is not a functionDas korrekte Argument gegen ein Downgrade des gekoppelten Pakets ist dieser Laufzeit-Bruch, nicht die Behauptung, es reintroduziere die ursprüngliche CVE: Ein älteres minimatch-Release löst seine eigene brace-expansion-Dependency typischerweise weiterhin über seinen eigenen deklarierten Bereich auf eine bereits gefixte Version innerhalb der Major-Version auf, sodass ein Downgrade die Verwundbarkeit nicht zwingend zurückbringt, sondern einen anderen Bug einführt.
Standardhaltung: Dieses Override ist eine Zurückstellung, keine gleichrangige Fix-Aktion. Gemäß "Majors zurückstellen" oben gehört eine Cross-Major-Zwangssubstitution in einen separaten, explizit reviewten Upgrade-Task, genau wie jeder andere breaking Major, selbst wenn ein Consumer tatsächlich ohne In-Major-Ausweg auf der vollständig verwundbaren Major-Linie feststeckt. Das Override innerhalb eines ansonsten routinemäßigen Lockfile-Fixes nur mit einem expliziten, dokumentierten Sign-off von der für diese Risikoentscheidung zuständigen Stelle anwenden, und in dieser Anfrage beide Vorbedingungs-Prüfungsergebnisse zitieren. Eine bestehende Testsuite ist kein Ersatz für beide Prüfungen; siehe "Eine grüne Suite ist kein Beleg für ein Override" unten.
Review-Checkliste
Für jeden Lockfile-CVE-Fix einen Review-Durchgang starten, nie überspringen, auch nicht für eine reine Lockfile-Änderung. Den Reviewer briefen, Folgendes zu verifizieren, mit Schweregrad pro Befund und einem expliziten Approve-/Request-Changes-Urteil:
- Scope: Der Diff ist nur Lockfile (plus minimale direkte
package.json-Dependency-Bumps); keine unabhängigen Änderungen, keine Quellcode-Edits. - CVE-Behebung: Die adressierten Advisories sind tatsächlich behoben (die gefixte Version wird tatsächlich erreicht).
- Keine Majors: Kein Semver-Major-Bump ist unangekündigt mit hineingerutscht.
- Lockfile-Kohärenz: Der verpflichtende
npm ci --dry-run-Sync-Check (sauberer Checkout, CI-npm-Version,--ignore-scripts, Attributions-Kontrolle gegen die Baseline vor dem Fix; Schritt 4 oben) ist tatsächlich für jeden betroffenen Baum gelaufen und bestanden, oder ein Baseline-FAIL wurde als vorbestehend geloggt, statt akzeptiert oder dem Fix angelastet zu werden. Den Beleg genau in dieser Form verlangen: den ungepipten Exit-Code, direkt nach dem blankennpm ci --dry-run-Befehl erfasst, plus dienpm -v-Ausgabe aus demselben Shell-Aufruf, pro Baum zitiert. Dienpm -v-Ausgabe bedeutet nur etwas, wenn die CI-Version-Aktivierung (Schritt 2) zuvor in derselben Shell lief; Belege aus einer Shell ablehnen, in der diese Aktivierung nicht bestätigt werden kann. Für von Hand eingepflegte Einträge zusätzlich Belege für alle drei Prüfungen in Schritt 3 verlangen, einschließlich des Exit-Codes vonnpm cimit leerem Cache unter der CI-npm-Version und des beidseitigen npm-10-Normalisierungsdiffs. Ein sauberesnpm auditauf demselben Lockfile ist kein Beleg dafür; es führt nie eine Installation aus. - Nichts ist mitgefahren: Der Diff schließt die adressierten Advisories und lässt keine unabhängigen Produktions-Dependencies mit driften.
npm audit fixnimmt jeden Bump innerhalb der Major-Version, den es kriegen kann, nicht nur die Advisory-Pakete. - Keine entfernten verschachtelten Einträge: Kein in der Basis vorhandener verschachtelter Lockfile-Eintrag ist verschwunden (siehe Schritt 5).
- Der eigene Build des Repositorys, nicht nur CIs Job-Liste: Die Build- und Test-Befehle direkt ausführen, nicht nur die von CI zufällig abgedeckte Teilmenge. Ein Workspace, den CI nicht baut oder testet, kann kaputt gemergt werden, während jeder CI-Check grün bleibt.
Eine grüne Suite ist kein Beleg für ein Override. Wenn der Fix ein overrides-Eintrag ist, der für manche Consumer eine Major-Grenze überschreitet, beweist eine bestehende Testsuite (oder ein Lint-Lauf, der den betroffenen Pfad nie berührt) nichts: Der Bruch ist latent und tritt nur auf, wenn eine konkrete Regel/Option/Verzweigung läuft. Wenn ein Reviewer ein solches Override absegnet, beide Vorbedingungs-Prüfungsergebnisse als zitierten Beleg verlangen, keinen Suite-Durchlauf: das Ergebnis der ESM-Import-Prüfung und das Ergebnis der Erreichbarkeitsprüfung gegen die eigene Konfiguration des Repositorys mit dessen gelinteter Dateizahl, plus Bestätigung, welche erfassten Call-Sites von den in diesem Repository aktivierten Regeln oder Optionen aus tatsächlich erreichbar sind. Ein sauberer Test- oder Build-Lauf ist eine andere Eigenschaft und kein Ersatz für eine der beiden Prüfungen.
Parallele Reviewer isolieren: kein geteiltes Scratchpad, keine generischen Dateinamen. Mehr als einen Review-Durchgang gleichzeitig laufen zu lassen macht aus einem geteilten Scratchpad plus generischem Dateinamen eine Kollision: Zwei Reviewer, die eine Baseline in denselben festen Pfad extrahieren, können sich gegenseitig stillschweigend den Extract überschreiben, und ein Review, das eine saubere Negativkontrolle meldet, liest womöglich tatsächlich die Baseline eines anderen Repositorys. Das ist leicht zu übersehen, wenn jedes geprüfte Repository dasselbe Advisory trägt, da die falsche Baseline weiterhin plausibel aussieht.
Jedem Reviewer ein eigenes Verzeichnis für alles geben, was er extrahiert (ein frisches mktemp -d, nie ein fester Name in einem geteilten Scratchpad), und Identität an Repository und Ref binden, bevor einer extrahierten Baseline vertraut wird: die aufgelöste Commit-SHA für den Ref bei jedem Extract festhalten und gegen den beabsichtigten Ref prüfen, oder den Extract <repo>-<ref> statt eines generischen Dateinamens nennen. Das eigene name-Feld eines Lockfiles reicht allein nicht aus: Es unterscheidet nur Repositories voneinander und ist identisch zwischen der Baseline vor dem Fix und dem Fix-Branch desselben Repositorys.
Anti-Patterns
- Einem sauberen
npm audit --package-lock-onlyoder einem byte-identischen No-op vonnpm install --package-lock-onlyals Beleg dafür vertrauen, dass das Lockfile installierbar ist. Keins von beiden führt eine Installation aus; nurnpm ci --dry-runtut das. - "
npm cilässt das Lockfile byte-identisch" als Kohärenzbeleg behandeln.npm cischreibt das Lockfile nie, unabhängig von dessen Inhalt. - Einen von Hand eingepflegten Integrity-Wert mit warmem npm-Cache prüfen; zwischengespeicherte Inhalte können einen falschen Wert verdecken. Für die echte
npm ci-Prüfung einen frischen leeren Cache verwenden. - Das Kohärenz-Gate unter der lokal gerade aktiven npm-Version statt unter der von CI gepinnten npm-Major-Version ausführen. Zwei npm-Majors können sich über dasselbe Lockfile uneinig sein.
- Einen Exit-Code hinter einer Pipe erfassen (
... | tail -N; echo $?), statt ihn direkt vom blanken Befehl zu erfassen. - Dem "switched to"-Banner eines Versionsmanagers vertrauen, statt
npm -vin genau der Shell auszugeben, die den Fix ausführt. - Eine npm-Major-Version, die älter ist als der Writer des Lockfiles, das Lockfile umschreiben lassen, was unbeteiligte
libc-Arrays entfernt, statt den einzelnen Zieleintrag von Hand einzupflegen und gegen die Registry zu prüfen. npm audit fixstillschweigend einen verschachtelten Lockfile-Eintrag entfernen lassen, ohne die Package-Keys danach gegen die Basis vor dem Fix zu diffen.- Eine "nur Dev"-Schlussfolgerung von einem Repository auf ein anderes übertragen, ohne das eigene
dev-Flag des Lockfiles im jeweiligen Repository erneut zu prüfen. - Ein Cross-Major-
overrides-Rezept als Standard-Fix statt als Zurückstellung anwenden, oder es anwenden, ohne beide Vorbedingungs-Prüfungen auszuführen und ein explizites, dokumentiertes Sign-off einzuholen. - Eine bestehende Testsuite als Beleg dafür behandeln, dass ein Cross-Major-Override sicher ist, wenn der geprüfte Bruch latent ist und nur über eine konkrete Regel, Option oder einen Codepfad erreichbar.
- Den Review-Durchgang für einen reinen Lockfile-Diff überspringen, in der Annahme, eine mechanisch aussehende Änderung brauche kein Review.
- Zwei Review-Durchgänge parallel gegen einen geteilten Scratchpad-Pfad laufen lassen und damit riskieren, dass ein Baseline-Extract stillschweigend überschrieben wird.