Five Critical RCEs in Dokploy: one bad habit, five open doors
Five CVSS 9.9 command-injection bugs in Dokploy, the self-hosted PaaS. Same mistake, five different features, five authenticated paths to running whatever you want on the box.
Self-hosted PaaS tools live a stressful life. We hand them the keys to everything. They build images, run containers, hold deploy keys and cloud credentials, and shell out to system tooling roughly every four seconds. Then we put them on the public internet behind one admin login and hope for the best. That is a lot of trust for one Node process, and it usually rewards a closer look.
Dokploy rewarded the look five times.
This is a structured summary of five vulnerabilities I reported in Dokploy, an open-source deployment platform. All five were accepted, got CVE IDs, and were credited to the GhostVirus byline. The advisories are public, so this stays at the level they do. No weaponized detail, no copy-paste exploit. Just the shape of the problem and how to not ship it.
Same mistake, wearing five different hats
Here is the fun part. All five findings are the exact same bug class: CWE-78, OS command injection, rated CVSS 9.9 Critical, all in Dokploy before 0.29.8.
The move never changes. Some value a user controls (a Git URL, a compose path, a certificate path, a database name, a Bitbucket repo) gets dropped straight into a shell command run through /bin/sh -c. No escaping. No allowlist. Just vibes. The shell reads your metacharacters, decides they look important, and runs them at deploy time.
Any single one of these is a bug. Finding the same anti-pattern in five unrelated features, each reachable by an authenticated low-privilege member, each capable of jumping across tenants when an owner or admin of another org gets involved, is not a bug anymore. That is a habit.
| CVE | Where it lives |
|---|---|
| CVE-2026-72740 | SSH customGitUrl domain, handed to ssh-keyscan |
| CVE-2026-72865 | composePath, dropped into docker compose -f … |
| CVE-2026-72880 | certificatePath, file write plus a remote mkdir -p … |
| CVE-2026-72733 | databaseName and backupFile, in the restore pipeline |
| CVE-2026-72872 | Bitbucket owner and repository, in git clone |
The five doors
CVE-2026-72740, the Git keyscan
Point Dokploy at a custom Git source over SSH and it runs ssh-keyscan against the domain it parses out of your URL. The regex that pulls out that domain is, let us say, relaxed. It happily lets shell metacharacters through, and the domain lands in the command unquoted. So the “domain” runs a command. Member-level deploy permission is all you need to knock.
CVE-2026-72865, the compose path
composePath gets stored and then poured directly into docker compose -f ${composePath}, docker stack deploy -c ${composePath}, and a touch for good measure. The validation on this field is z.string().min(1), which checks that you typed at least one character. Bold. Everything runs through /bin/sh -c, so a path with the right punctuation is a command. It all lives in packages/server/src/utils/builders/compose.ts if you want to see it in daylight.
CVE-2026-72880, the certificate path (buy one bug, get one free)
certificatePath is settable by the client and then used, unsanitized, in two different places. Feed it ../ and you get arbitrary file write anywhere the Dokploy process can reach, which includes the fun directories like startup and cron. Bind that certificate to a remote server and the same value shows up unquoted in a remote mkdir -p ${certDir}, which gets you command execution on the remote box too. One field. A file-write primitive and an RCE primitive. Efficient.
CVE-2026-72733, the database restore
The restore feature interpolates databaseName unquoted into a docker exec, then tucks backupFile inside double quotes in an rclone argument, apparently forgetting that $(...) still expands inside double quotes. The best detail is that it does not even ask for a real target. The container-search pipeline ends in | head -n 1, so you do not need a valid container or a valid backup file for your injected command to run. You just need member-level restore permission and a sense of humor.
CVE-2026-72872, the Bitbucket provider
bitbucketOwner and bitbucketRepository go straight into a git clone with no validation. What makes this one sting is the comparison. The GitHub, GitLab and Gitea providers all validate credentials through an API call first. The Bitbucket provider skips that and builds the command straight from stored values, which means it works with completely fake credentials. Somebody wrote the safe version three times and then wrote the fourth one differently.
Disclosure
All five went through coordinated disclosure. Reported privately, acknowledged by the maintainers, fixed in Dokploy 0.29.8 before any public detail landed, CVE IDs assigned for each. Real credit to the Dokploy team, who were quick and professional the whole way through. Fixing five criticals without drama is not nothing.
The moral of the story
None of these are clever. They are the classic failure mode of software that is moving fast: input gets treated as trustworthy because it came from a logged-in user, and a shell gets used where a plain argument array would have done the job quietly.
The fix is the same every time, and it is boring on purpose. Build commands with execFile or spawn and argument arrays instead of gluing strings together. Validate hostnames, paths and names against a strict allowlist and throw out .., absolute paths and metacharacters. Generate sensitive paths on the server instead of trusting the client to hand you a nice one. Check tenant isolation on every privileged action, not just the first one.
Five features, five times, the same guardrail missing. Writing the pattern down by name is how it stops showing up, here and everywhere else in the self-hosted world that reaches for a shell to get through the day.
Byline: GhostVirus, ProjectMerai. Reported under coordinated disclosure. The linked GitHub Security Advisories are the record that counts.
Written by
GhostVirus
Offensive-security research at ProjectMerai. Published. Findings are proven before they are published: we confirm before we claim.
ghostvectoracademy