
DbGateのloadReaderエンドポイントにおけるfunctionNameインジェクション経由のリモートコード実行 — CVSS 8.8
functionName インジェクションによる DbGate のリモートコード実行深刻度: 高 (CVSS 8.8)
CWE: CWE-94 — コード生成の不適切な制御 (「コードインジェクション」)
影響範囲: dbgate-api ≤ 7.1.8 (7.1.9 で修正済み)
アドバイザリ: GHSA-hv83-ggc4-v385
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-48017
クレジット: Romain Deperne
POST /runners/load-reader エンドポイントは functionName パラメータを受け取り、それを
JavaScript のテンプレート文字列に直接補間したうえで、フォークされたプロセス内で実行します — サニタイズも
検証も一切ありません。認証済みの任意のユーザー (特別な権限は不要) が意図された呼び出しから抜け出し、
任意の JavaScript を実行でき、サーバー上でのリモートコード実行が可能になります。
フォークされたランナーはサンドボックスとして require = null を設定しますが、process.binding("spawn_sync") には依然として到達可能です:
process.binding("spawn_sync") は引き続き到達可能であり、実際の OS プロセスを起動します。
私は DbGate のランナーサブシステムを、ガードされたコード実行パスとガードされていないコード実行パスの
ギャップという観点で監査していました。runners.js:292 にある start() ランナーは適切にゲートされています — これは
testStandardPermission('run-shell-script') を呼び出し、platformInfo.allowShellScripting をチェックします。
loadReader() は概念的には同様の処理 (JS ローダースクリプトを構築して実行する) を行いますが、
そうしたチェックを一切備えていません。私はデータフローを追跡しました:
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 は攻撃者によって制御され、実行されるテンプレート文字列の中に埋め込まれます。その前に置かれているのは dbgateApi. というプレフィックスのみであり、これはエスケープ可能です。式を toString(); で閉じ、ペイロードを注入し、末尾の (${props}) を // でコメントアウトすることで、有効な JS が生成されます。
ファイル: packages/api/src/controllers/runners.js (loadReader → loaderScriptTemplate)
ファイル: 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)});`
生成された(注入された)スクリプト:
const reader = await dbgateApi.toString();
process.binding("spawn_sync").spawn({ file:"/bin/sh", args:["/bin/sh","-c","id"], ... });
dbgateApi.toString//({});
compileShellApiFunctionName() は短い名前を完全修飾された API パス
(dbgateApi.<name>) に変換するために書かれており、functionName が識別子であることを暗黙的に信頼していました。しかし、この値は
HTTP リクエストボディから直接取得されます。これが後で
eval/fork されるソースに連結されるため、この信頼の前提は RCE となります。require = null サンドボックスは役に立ちません — Node の
内部 process.binding("spawn_sync") がこれをバイパスします。
修正 (7.1.9): ローダースクリプトにコンパイルする前に、functionName を厳格な識別子許可リストに対して検証します。
poc/rce_loadreader_functionname_injection.py — 実際のエンドポイントをエンドツーエンドで駆動します。
# 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'
スクリプトは functionName ペイロードを構築し、それを POST /runners/load-reader に送信すると、注入された spawn_sync 呼び出しがフォークされたランナープロセス内でコマンドを実行します。
DbGate API ホスト上での認証済み RCE。DbGate はデータベース管理 GUI であり、内部データベースへの広範なネットワーク到達性を伴ってデプロイされることが多いため、その上でのコード実行はデータ層への強力な足がかりとなります。
責任ある開示として公開。PoC は修正版リリース後に、防御者および検知エンジニアリングのために公開されました。