Type a semicolon into a text box, wait for someone to click "start," and own the box. That's CVE-2026-27130 in one sentence: a low-privileged, authenticated user on Dokploy — a self-hosted PaaS with 31,000+ GitHub stars — types a shell metacharacter payload into the "app name" field when creating a new application. Nothing happens immediately. The payload sits in the database until the app is started, stopped, removed, or scaled, at which point Dokploy's control plane shells out with that raw string and the payload runs with the same privileges as the daemon managing every other app on the box. CVSS score: 9.9, one-tenth of a point short of the maximum possible.
That score is the headline, but the more useful story is what the vulnerability's anatomy — and its own history — reveal about a bug class that self-hosted PaaS tools keep shipping.
Anatomy of CVE-2026-27130
The vulnerable field is appName, submitted when a user creates a new application through Dokploy's UI or API. Three separate failures had to line up for it to become exploitable:
- Inadequate input sanitization. Dokploy's
cleanAppNamefunction — the one function whose entire job was to make this string safe — replaces spaces and lowercases the input. That's it. Shell metacharacters (;,$(), backticks,|,&) pass straight through untouched. - No schema validation. Nothing at the API boundary constrained
appNameto a safe character set before it reached the database. - Direct shell interpolation. When a service operation runs — start, stop, remove, scale — Dokploy calls
execAsync()orexecAsyncRemote()with the app name interpolated directly into a shell command string, rather than passed as a discrete argument.
The exploit doesn't even need to fire at creation time. An attacker plants the payload in appName, and it lies dormant until any later operation on that app triggers the shell call — meaning the "attack" and the "detonation" can be separated by hours or days, and the person who eventually triggers it (an admin restarting a stuck service, an autoscaler scaling it) doesn't have to be the attacker.
The full CVSS 3.1 vector is AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H — network-reachable, low attack complexity, no user interaction, full compromise of confidentiality/integrity/availability, and a scope change (the exploit escapes the vulnerable component's own security context). The one clause worth correcting against how this class of bug often gets described: PR:L means it requires low-privileged authentication, not zero authentication. That's an important distinction, but not a comforting one for a multi-tenant platform — it means any signed-up team member, including the newest, least-trusted account on the instance, already holds the one privilege the exploit needs.
Dokploy versions 0.26.6 and below are affected; the fix shipped in 0.26.7. The patch is a useful artifact in its own right: it added an allowlist regex, ^[a-zA-Z0-9._-]+$, enforced via schema validation across all seven places appName gets accepted — application, compose, and each of the four supported managed-database types. Allowlisting, not denylisting, and enforced at every entry point, not just the one that got reported.
This Isn't Dokploy's First Rodeo With appName
CVE-2026-27130 is not the first time this exact field caused this exact class of bug. Roughly ten months earlier, in July 2025, Dokploy shipped a fix for a related but distinct issue: an authenticated, low-privileged user — one not even belonging to any organization on the instance — could run arbitrary OS commands on the host through the docker.getContainersByAppNameMatch tRPC procedure, which interpolated the same unsanitized appName value into a Docker CLI call. That one was rated "moderate" rather than critical, fixed in 0.23.7.
Two CVEs, ten months apart, same field, same underlying assumption — that appName was safe to hand to a shell — and two different call sites where that assumption broke. Patching the docker.getContainersByAppNameMatch procedure in 2025 didn't touch the execAsync() call path that broke again in 2026, because the fix addressed one vulnerable function rather than the premise that user-supplied strings need validation at the boundary, before they reach any shell-adjacent code. That's the difference between closing a specific hole and closing the class of hole.
The Bug Class Is the Category, Not One Project
It would be easy to read the above as a Dokploy-specific lapse. It isn't. Coolify — the more mature, more widely deployed self-hosted PaaS in this category, with 51,000+ GitHub stars — has shipped a strikingly similar list of critical vulnerabilities across the same window:
| CVE | CVSS | Root cause |
|---|---|---|
| CVE-2025-59156 | 9.4 | Low-privileged member injects arbitrary Docker Compose directives during project creation; a malicious service definition mounts the host filesystem, escaping container isolation entirely |
| CVE-2025-64419 | 9.7 | Command injection via docker-compose.yaml; arbitrary system commands execute as root |
| CVE-2025-64420 | 10.0 | Low-privileged user can read the host's private SSH key, then authenticate to the server as root over SSH |
| CVE-2025-66209 – 66213 | 10.0 | User input reaches shell commands unsanitized; RCE up to root |
| CVE-2026-34158 | 8.8 | A user able to edit application settings escapes container confinement and executes arbitrary commands on the managed host |
As of January 8, 2026, security researchers counted roughly 52,890 Coolify instances reachable from the public internet. Every one of the vulnerabilities above shares the same shape as Dokploy's: a field a low-privileged, authenticated user controls flows, unsanitized, into an operation that a root-privileged daemon executes on the host.
Two independently-built projects, converging on the same failure mode from different codebases, is what makes this a category problem rather than a code-review lapse at one company. Self-hosted PaaS tools all share a structural feature that makes this bug class unusually dangerous where it appears: the control plane needs host- or Docker-daemon-level privilege to do its job, and every text field a tenant can fill in is a potential path into that privilege if the input isn't treated as hostile at the boundary.
Why the Deploy Tool Is the Highest-Value Target on the Box
It's worth being precise about why this keeps happening in this specific category of software, because the answer isn't "these teams write careless code" — it's architectural.
Dokploy's own install script requires root and, as part of setup, mounts the host's Docker socket directly into the Dokploy container (--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock). That's not incidental — it's the mechanism that lets Dokploy start, stop, and inspect every container on the box, including every tenant's app. Coolify's architecture makes the same trade for the same reason.
That means the control plane already holds host-level power before any bug exists. A self-hosted PaaS's threat model usually focuses on isolating tenant workloads from each other — network policies, resource limits, maybe a gVisor or Kata sandbox around the untrusted code a tenant pushes. All of that isolation is irrelevant to this bug class, because the attacker never touches a tenant's sandboxed container. The attacker touches a text field in the control plane's own API, and the control plane is the thing already sitting outside every sandbox, holding the Docker socket. One compromised input field in the deploy layer beats an arbitrarily well-isolated fleet of tenant containers, because the deploy layer was never inside the isolation boundary to begin with — it's what draws the boundary.
What a Cluster-API Control Plane Needs to Not Ship the Same Bug
The concrete discipline this bug class demands isn't exotic — it's the same handful of practices, applied consistently at every boundary where user input can reach a privileged operation:
- Allowlist, not denylist, at the API schema layer. Dokploy's fix —
^[a-zA-Z0-9._-]+$enforced via schema validation — is the right shape: reject anything outside a known-safe character set, rather than trying to strip or escape known-dangerous characters. Denylisting loses the arms race the moment someone finds a metacharacter the list didn't anticipate. - Never build a shell command by string interpolation.
execAsync()and its Node.js equivalents (exec(), template-literal shell strings) hand the shell a string to parse, and any unescaped metacharacter in that string is live. Spawning via an argv array (execFile()/spawn()with an arguments list, not a single command string) means the shell never re-parses attacker-controlled text — the app name becomes an inert argument, not executable syntax. - Validate at every entry point that field can reach, not just the one that got reported. CVE-2025-53376 and CVE-2026-27130 are the same lesson twice: a fix scoped to one function, rather than to the field itself, leaves every other call site that touches the same value exposed. The fix that finally worked touched seven schema files, not one.
- Treat "internal" input the same as external input. An app name typed by an authenticated team member is still attacker-controlled input the moment that account can be compromised, phished, or is simply the newest and least-trusted seat on the instance.
PR:Lshould not read as "safe because it's authenticated" — low privilege is often exactly what a real attacker starts with. - Add "does this reach a shell-out" to code review, explicitly. Every one of the CVEs above is a variant of the same root cause reaching production repeatedly across two separate projects. That's not a knowledge gap — teams clearly know sanitization matters, given how many of these fixes are schema validation added after the fact — it's a discipline gap in catching it before merge. A control plane that manages a Cluster API fleet on owned hardware carries exactly the same structural risk Dokploy and Coolify do: it needs root-equivalent access to manage machines, which makes every field in its own API a candidate for this bug class, checked case by case rather than assumed safe by default.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. A control plane with this much power over a fleet is exactly the kind of surface that has to treat every input field as hostile by default. Star the repo on GitHub or deploy your first app today.