
Exécution de code à distance dans DbGate via une injection de functionName dans le point de terminaison loadReader — CVSS 8.8
functionNameGravité : Élevée (CVSS 8.8)
CWE : CWE-94 — Contrôle impropre de la génération de code (« Injection de code »)
Affecté : dbgate-api ≤ 7.1.8 (corrigé dans 7.1.9)
Avis : GHSA-hv83-ggc4-v385
NVD : https://nvd.nist.gov/vuln/detail/CVE-2026-48017
Crédit : Romain Deperne
Le point de terminaison POST /runners/load-reader prend un paramètre functionName et l'interpole
directement dans une chaîne de modèle JavaScript qui est ensuite exécutée dans un processus forké — sans aucune
assainissement ni validation. Tout utilisateur authentifié (aucune permission spéciale requise) peut sortir de
l'appel prévu et exécuter du JavaScript arbitraire, réalisant ainsi une exécution de code à distance sur le serveur.
Le runner forké définit require = null comme bac à sable, mais process.binding("spawn_sync") reste accessible :
process.binding("spawn_sync") est toujours accessible et lance un véritable processus du système d'exploitation.
J'auditais le sous-système de runners de DbGate pour l'écart entre les chemins d'exécution de code protégés et non protégés.
Le runner start() à runners.js:292 est correctement protégé — il appelle
testStandardPermission('run-shell-script') et vérifie platformInfo.allowShellScripting.
loadReader() fait la même chose conceptuellement (il construit et exécute un script de chargement JS) mais n'a
aucun de ces contrôles. J'ai tracé le flux de données :
runners.js:353 loadReader({ functionName, props })
runners.js:366 loaderScriptTemplate(prefix, functionName, ...)
runners.js:64 `... ${compileShellApiFunctionName(functionName)}(${JSON.stringify(props)});`
packageTools.ts:33 return `dbgateApi.${functionName}` // ← no sanitization
functionName est contrôlé par l'attaquant et se retrouve à l'intérieur d'une chaîne de template exécutée. Le préfixe dbgateApi.
est la seule chose qui le précède, et il est échappable : fermer l'expression avec toString();,
injecter la charge utile, et commenter le (${props}) final avec // produit du JS valide.
Fichier : packages/api/src/controllers/runners.js (loadReader → loaderScriptTemplate)
Fichier : packages/tools/src/packageTools.ts:33 (compileShellApiFunctionName)
// packageTools.ts:33 — functionName flows in unsanitized
return `dbgateApi.${functionName}`;
// runners.js:64 — interpolated into the executed loader template
`const reader = await ${compileShellApiFunctionName(functionName)}(${JSON.stringify(props)});`
Script généré (injecté) :
const reader = await dbgateApi.toString();
process.binding("spawn_sync").spawn({ file:"/bin/sh", args:["/bin/sh","-c","id"], ... });
dbgateApi.toString//({});
compileShellApiFunctionName() a été écrit pour transformer un nom court en un chemin d'API pleinement qualifié
(dbgateApi.<name>), en faisant implicitement confiance à functionName pour être un identifiant. La valeur, cependant,
provient directement du corps de la requête HTTP. Parce qu'elle est concaténée dans du code source qui est ensuite
eval/forké, l'hypothèse de confiance constitue une RCE. Le bac à sable require = null n'aide pas — le
process.binding("spawn_sync") interne de Node le contourne.
Correctif (7.1.9) : valider functionName par rapport à une liste d'autorisation d'identifiants stricts avant de le compiler
dans le script du loader.
poc/rce_loadreader_functionname_injection.py — pilote le véritable point de terminaison de bout en bout.
# with an existing JWT
python3 poc/rce_loadreader_functionname_injection.py http://localhost:3000 <JWT> 'id > /tmp/pwned'
# or log in first
python3 poc/rce_loadreader_functionname_injection.py http://localhost:3000 --login admin password 'id'
Le script construit la charge utile functionName, l'envoie à POST /runners/load-reader, et l'appel spawn_sync injecté exécute la commande dans le processus runner forké.
RCE authentifié sur l'hôte de l'API DbGate. DbGate est une interface graphique de gestion de bases de données fréquemment déployée avec une large portée réseau vers des bases de données internes — l'exécution de code sur celle-ci constitue un puissant point de pivot vers la couche de données.
Divulgué de manière responsable. PoC publié après la livraison du correctif, à destination des défenseurs et de l'ingénierie de détection.