At a glance
- Product: Dokploy, a self-hostable Platform-as-a-Service (PaaS)
- Affected versions: prior to 0.29.13
- Fixed in: 0.29.13
- Covered here: CVE-2026-72740, CVE-2026-72733, CVE-2026-72865, CVE-2026-72872, CVE-2026-72880 (all CVSS 9.9, Critical)
- Class: OS command injection (CWE-78); one path-traversal file write
- Prerequisite: an authenticated organization member holding a routine, scoped permission
- Impact: member-level access escalates to code execution on the Dokploy host
- Disclosure: reported privately via GitHub Security Advisories and fixed before public disclosure; no indication of exploitation in the wild
Identifying and responsibly disclosing vulnerabilities in widely used software is part of how Versa’s security research team strengthens the ecosystem our customers depend on, and how we turn emerging threats into protection inside our own platform. This write-up has two parts. The first, below, details a set of critical remote-code-execution vulnerabilities our team discovered and reported in Dokploy. The second, authored by our threat-research team, shows how Versa detects and blocks attempts to exploit them through signature-based coverage.
Five Critical Dokploy Vulnerabilities
Dokploy is a self-hostable Platform-as-a-Service that automates the deployment of applications and databases. To do that, it runs shell commands on the host on the operator’s behalf. The five vulnerabilities below, all patched in v0.29.13 and all rated CVSS 9.9, share a single root problem: at various points, Dokploy also ran attacker-supplied commands on the host, because user input was interpolated into shell strings without validation or quoting. All five were reported privately to the Dokploy maintainers and were fixed before this write-up was published.
This research covered ten vulnerabilities reported in the 0.29.13 release; the five detailed here are the most instructive, in part because four of them are the same defect appearing in different code paths. A single recurring pattern is more useful to study than any one isolated exploit, because it points at a systemic fix and, as the detection section will show, a systemic basis for coverage.
The Recurring Pattern
Four of the five center on execAsync and its remote counterpart execAsyncRemote, helpers that take a string and execute it in a shell. These functions do not validate their input; that responsibility belongs to the caller. Across the codebase, callers assembled command strings by placing user-controlled fields directly into them. The shell, doing exactly what shells do, interpreted the metacharacters. The result was OS command injection (CWE-78) reachable by an authenticated member with a routine, scoped permission.
CVE-2026-72740: customGitUrl in ssh-keyscan
In packages/server/src/utils/providers/git.ts, the user-controlled customGitUrl is processed by sanitizeRepoPathSSH and its domain is then interpolated into the ssh-keyscan command built by addHostToKnownHostsCommand without shell quoting. A member with service-deployment permission and an attached SSH key can execute arbitrary commands on the host during deployment. The sanitizer runs, but it does not quote its output before that value reaches the shell.
CVE-2026-72733: databaseName / backupFile in database restore
The backup.restoreBackupWithLogs tRPC subscription builds restore pipelines from two user-controlled fields. restore/utils.ts interpolates databaseName into database-specific restore commands, and restore/postgres.ts (with its siblings) interpolates backupFile into rclone paths. Both fields are independently injectable, and the injection fires even when no valid database container or backup file exists; the command runs before either is validated. An authenticated member with backup-restore permission gains host-context execution.
CVE-2026-72865: composePath in compose operations
compose.update stores an unvalidated composePath, which is then interpolated into docker compose -f, docker stack deploy -c, and a touch command, each executed through /bin/sh -c. A member with compose write and deploy permission can trigger compose.deploy or startCompose and execute commands. The impact is amplified by context: the Dokploy container has access to the host Docker socket, so code execution inside it is effectively code execution on the host.
CVE-2026-72872: Bitbucket owner / repository in git clone
application.saveBitbucketProvider stores bitbucketOwner and bitbucketRepository without validation, and cloneBitbucketRepository in bitbucket.ts interpolates them into a git clone command. The consequence is that the Bitbucket account name and repository name are themselves injection points, fields that are not instinctively treated as attacker-controlled, which is precisely why they went unguarded.
The Outlier – CVE-2026-72880: certificatePath traversal
This one is not command injection. The apiCreateCertificate schema accepts a client-supplied certificatePath, and services/certificate.ts joins it to the certificate root with no path confinement. A member with certificate create or delete permission can write attacker-controlled content outside the intended directory, or delete an out-of-root directory. On its own, an arbitrary file write is not a boundary; it is a primitive. On a host running multiple applications and databases, a well-placed write is a credible path to full server takeover. It is included here because it reaches the same outcome as the other four (host compromise) through a different mechanism: a reminder that the class of a bug matters less than the trust boundary it crosses.
The Part Worth Dwelling On: Privilege Escalation
None of these are pre-authentication. Every one requires an authenticated organization member holding a specific, ordinary permission: deploy a service, restore a backup, edit a compose file, create a certificate. That is the actual finding.
Dokploy is multi-tenant. Its RBAC model is intended to keep a member of one organization within their own lane, able to manage their own services without touching the host or their neighbors. These vulnerabilities show that a scoped, member-level permission repeatedly escalated into host-level code execution in a Docker-privileged container. The separation between “member of org A” and “root on the shared host” turned out to be one unquoted variable thick. In a single-tenant deployment that is a serious bug; in a multi-tenant one it is a tenant-isolation failure.
Fixing Dokploy Vulnerabilities
The remediation across the set was consistent: allow-list input validation using Zod schemas, following the pattern the codebase already used for VALID_BRANCH_REGEX: define what a legal value looks like and reject everything else, rather than stripping dangerous characters after the fact. This is the right approach. Deny-by-default validation at the schema boundary is more robust than sanitization, and it applies uniformly to every field instead of requiring each call site to remember to quote.
The broader lesson is a familiar one, which is part of why it is worth restating: untrusted input does not belong in a shell command string, and “sanitize” is not a synonym for “validate against an allow-list.” Both mistakes were present here, both are common, and the defensive pattern that fixed them already existed in the same repository.
Responsible Disclosure
All issues were reported privately to the Dokploy maintainers through GitHub Security Advisories, with one advisory per CVE, and were remediated in the 0.29.13 release before public disclosure. We commend the Dokploy team for their prompt response and for adopting schema-level allow-list validation as a durable fix rather than a set of point patches.
- Reported to maintainer: July 2026
- Fix released (v0.29.13) and advisories published: August 2026
- Disclosure channel: GitHub Security Advisories (GitHub as CNA)
From Disclosure to Protection
Upgrading to Dokploy 0.29.13 removes the root cause, but every environment upgrades on its own schedule, and the window between public disclosure and a fully patched fleet is exactly when exploitation attempts tend to appear. That gap is where network-level detection earns its place: it keeps customers protected while they roll out the fix. Because four of these five findings share one underlying pattern (user input reaching a shell), they also lend themselves to coverage that generalizes rather than a separate rule per CVE. The section that follows details how Versa provides that coverage.
Detection and Coverage with Versa
How the Signatures Work
The five CVEs split naturally into two detection problems, and the signatures reflect that.
Versa Networks Protections Against Dokploy Command Injection and Path Traversal
Available from Versa Spack: 2377
| SID (Signature Identifier) | CVE(s) Covered |
|---|---|
| 1000032727 | CVE-2026-72739, CVE-2026-72736, CVE-2026-72740, CVE-2026-72865, CVE-2026-72868, CVE-2026-72869, CVE-2026-72870, CVE-2026-72872 |
| 1000032728 | CVE-2026-72880 |
Four of the five (CVE-2026-72740, CVE-2026-72865, CVE-2026-72872, and CVE-2026-72733) are the same defect in different places: user input reaching a shell via execAsync. That structural similarity means they can be covered by a single rule rather than one per CVE. Rule 1000032727 does exactly that. It anchors POST requests to /api/trpc/ and then applies two sequential PCRE checks. The first identifies the specific tRPC endpoints that are vulnerable: compose.update, application.saveBitbucketProvider, backup.restoreBackupWithLogs, and others. The second looks inside the request body for the specific field names that feed into shell commands: customGitUrl, composePath, bitbucketOwner, databaseName, and so on, and checks whether their values contain shell metacharacters: backticks, semicolons, pipes, or $. Those are the characters that turn a string into a command. The rule fires when both conditions are true: the right endpoint is being called, and the right field contains something that looks like an injection. This approach, endpoint specificity plus field-level pattern matching, keeps the false-positive rate low while ensuring the rule generalizes across all four injection paths.
CVE-2026-72880 is different. There is no shell involved: the vulnerability is a path traversal that lets an attacker write files outside the intended directory. Rule 1000032728 is scoped specifically to the certificates.create tRPC endpoint and looks at the certificatePath field for path traversal sequences in their various encoded forms: literal, URL-encoded, double-encoded, and their backslash equivalents. Encoding variants matter here because attackers routinely try to slip past pattern-matching by percent-encoding characters that would otherwise match. The rule accounts for all of them.
Key Takeaways
These five CVEs illustrate how a single recurring implementation mistake, trusting user input near a shell, can surface across an entire codebase and produce critical impact at scale. The vulnerabilities do not require credential theft, privilege escalation chains, or exotic techniques. They require a legitimate account and a routine permission that any organization member might hold. The separation between “member of an organization” and “root on the shared host” came down to one unquoted variable in each case.
The fix, allow-list validation at the schema boundary, already existed as a pattern in the same codebase. The broader lesson is that defense-in-depth matters: schema validation removes the root cause, but network-layer detection with Versa IPS provides a parallel layer of protection that holds during the patching window and beyond.