CVE-2026-54461 : Habitica, injection de regex et ReDoS dans la recherche de membres

CVE-2026-54461

Un champ de recherche de Habitica oublie d'échapper l'input sur le username : injection de regex, énumération et ReDoS.

Habitica est une appli de productivité gamifiée (open source, Node.js). L’endpoint GET /api/v3/groups/:groupId/members?search=... cherche des membres dans un groupe en construisant une regex MongoDB à partir du paramètre search. L’input est bien échappé pour un champ, mais pas pour l’autre (website/server/controllers/api-v3/members.js) :

if (req.query.search) {
  const escapedSearch = escapeRegExp(req.query.search);   // input nettoyé...
  query.$or = [
    { 'profile.name':        { $regex: new RegExp(escapedSearch, 'i') } },      // ...bien utilisé ici
    { 'auth.local.username': { $regex: new RegExp(req.query.search, 'i') } },   // mais ici : input BRUT
  ];
}

escapeRegExp (lodash) neutralise les métacaractères d’une regex. Il est appliqué correctement sur profile.name, mais la ligne du dessous réutilise req.query.search brut au lieu de escapedSearch. Donc sur le champ auth.local.username, tout ce qu’on met dans search est interprété comme une regex. La bonne variable existait déjà deux lignes plus haut, c’est juste la mauvaise qui a été passée.

Concrètement, pour n’importe quel utilisateur authentifié membre du groupe :

  • Énumération des usernames. Là où une recherche littérale ne renvoie rien, un motif regex remonte des membres : .* renvoie tout le groupe, ^admin révèle qu’un username commence par admin, (?=.*secret) teste une sous-chaîne. En enchaînant des motifs ancrés on reconstitue les usernames caractère par caractère.

  • ReDoS. Un motif comme (a+)+$ déclenche un backtracking catastrophique. La regex étant passée à MongoDB en $regex, c’est le moteur de la base qui l’évalue contre les usernames, et le temps de calcul croît exponentiellement avec la longueur du motif (de quelques dizaines de ms à plusieurs secondes). Une seule requête suffit à faire tourner le CPU de la base dans le vide et à figer la réponse ; le rate limiter (30 req/60 s) n’y change rien, quelques requêtes en parallèle suffisent.

Liens