# Authenticated Remote Code Execution via loadReader functionName code injection in DbGate

DbGate's /runners/load-reader interpolates the user-supplied functionName into generated JS, letting any authenticated user inject code and run OS commands as root.

- Payout: Research
- Severity: critical
- Program: DbGate
- Found by: tomasvanagas
- Disclosed: 2026-06-05
- CVE: CVE-2026-47670
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-wm5r-5qp3-5vxf
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-wm5r-5qp3-5vxf
- Tags: area:web domain:oss domain:web impact:privesc impact:rce kind:research platform:ghsa sev:critical tech:nodejs technique:incomplete-fix technique:source-review vuln:command-injection vuln:rce

**Target:** /runners/load-reader endpoint of dbgate-api (Node.js)
**Root cause:** The functionName parameter is interpolated directly into a dynamically generated JavaScript script template (`await dbgateApi.${functionName}({...})`) with no allowlist validation, so a newline breaks out of the expression into arbitrary code.
**Steps:**
1. Authenticate via `POST /auth/login` with `{"amoid":"logins","login":...,"password":...}` to obtain a JWT accessToken.
2. Craft functionName starting with a valid reader name plus newline, e.g. `csvReader\nvar _r = (await import('child_process')).execSync('<cmd>').toString();...`
3. POST to `/runners/load-reader` with `{"functionName": <payload>, "props": {}}` and `Authorization: Bearer <token>`.
4. Payload writes command output as JSONL to a temp file, then retrieve it via `POST /jsldata/get-rows` with `{"jslid":"file:///tmp/_rce_<id>.jsonl","offset":0,"limit":10000}`.
**How found:** Reporter noticed functionName was string-interpolated into a generated script template; the June 2025 fix only set `require = null`, which is bypassable because dynamic `import()` is a language keyword rather than a nullifiable function.
**Exploitation:** Any authenticated DbGate user sends a crafted functionName to /runners/load-reader; the injected JS uses `await import('child_process')` to bypass the `require = null` mitigation and calls execSync, writing output to a JSONL file read back through /jsldata/get-rows. Runs as root in the Docker image, yielding container root shell.
**Escalation:** Root in container enables reading /proc/1/environ for cloud tokens and LOGIN_PASSWORD_* of other users, network pivoting to internal services, and persistent backdoors via bundle.js modification or cron/SSH keys surviving restarts.
**Insight:** When a fix nullifies `require` to sandbox generated Node.js code, dynamic `import()` still works — always test import() against such mitigations. Also, any parameter interpolated into a code template (not just eval) is an injection sink; a newline is enough to break out of an expression.

## Proof of concept (copied exactly from the original report)

**HTTP request to load-reader endpoint**

```
r = requests.post(
            f"{base}/runners/load-reader",
            json={"functionName": payload_fn, "props": {}},
            headers=headers,
            timeout=35, verify=False,
        )
```

