CVE-2026-45628
Description
Dokploy is a free, self-hostable Platform as a Service (PaaS). In 0.29.2 and earlier, Dokploy constructs shell commands using JavaScript template literals and executes them via child_process.exec() (which runs through /bin/sh -c). User-supplied branch names, repository URLs, and Docker credentials are interpolated directly into these commands without escaping. This requires an authenticated user with application create/edit privileges.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Dokploy 0.29.2 and earlier suffers from command injection via unescaped user-supplied branch names and repository URLs, leading to RCE for authenticated attackers with application create/edit privileges.
Vulnerability
Dokploy versions 0.29.2 and earlier construct shell commands using JavaScript template literals and execute them via child_process.exec() (which passes through /bin/sh -c). User-supplied branch names, repository URLs, and Docker credentials are interpolated directly into these commands without shell escaping, despite the codebase already containing a shEscape() function in registry.ts that was not applied to the deployment pipeline. Affected components include the Git provider files (git.ts, github.ts, gitlab.ts) where commands like git clone --branch ${branch} ... are built. No unauthenticated path exists; the webhook deploy endpoint does not allow injection due to branch matching logic. [1]
Exploitation
An authenticated attacker with application create or edit privileges can inject shell metacharacters into the branch name or repository URL fields when creating or modifying an application. The attacker does not require any special network position beyond authenticated access to the Dokploy web interface or API. No user interaction is needed beyond the initial authenticated action. The injected payload is then passed to the shell execution pipeline during deployment, achieving remote code execution on the Dokploy server. [1]
Impact
Successful exploitation allows the attacker to execute arbitrary commands on the Dokploy server with the privileges of the container running the application (container root). Because Dokploy mounts the Docker socket, the attacker can escalate to full host compromise by running a privileged container (e.g., docker run -v /:/host --privileged busybox chroot /host), granting complete control over the host system. The impact includes full confidentiality, integrity, and availability loss of the Dokploy server and potentially the underlying host. [1]
Mitigation
As of the publication date (2026-05-29), no patched version has been released for CVE-2026-45628. The vendor has acknowledged the issue and is expected to release a fix that applies the existing shEscape() function to all shell-injected user inputs in the deployment pipeline. Until a fix is available, administrators should restrict application create/edit privileges to trusted users only, and monitor for unauthorized deployment activity. There is no known inclusion in CISA's Known Exploited Vulnerabilities catalog at this time. [1]
AI Insight generated on May 29, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
95e021797f3cdfeat(validation): standardize branch name validation across provider schemas
2 files changed · +2 −1
apps/dokploy/esbuild.config.ts+1 −0 modified@@ -28,6 +28,7 @@ try { "wait-for-postgres": "wait-for-postgres.ts", "reset-password": "reset-password.ts", "reset-2fa": "reset-2fa.ts", + "migrate-auth-secret": "migrate-auth-secret.ts", }, bundle: true, platform: "node",
apps/dokploy/package.json+1 −1 modified@@ -14,7 +14,7 @@ "wait-for-postgres-dev": "tsx -r dotenv/config wait-for-postgres.ts", "reset-password": "node -r dotenv/config dist/reset-password.mjs", "reset-2fa": "node -r dotenv/config dist/reset-2fa.mjs", - "migrate-auth-secret": "tsx -r dotenv/config scripts/migrate-auth-secret.ts", + "migrate-auth-secret": "node -r dotenv/config dist/migrate-auth-secret.mjs", "dev": "tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ", "studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts", "migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
547ba2d04bc6feat(validation): enhance registry URL validation in schema
2 files changed · +15 −4
packages/server/src/db/schema/registry.ts+14 −3 modified@@ -44,11 +44,22 @@ export const registryRelations = relations(registry, ({ many }) => ({ }), })); +// Registry URLs must be hostname[:port] only — no shell metacharacters +// Empty string is allowed (means default/Docker Hub registry) +const registryUrlSchema = z + .string() + .refine( + (val) => + val === "" || + /^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?(:\d{1,5})?$/.test(val), + "Registry URL must be a valid hostname or hostname:port (e.g. registry.example.com or localhost:5000)", + ); + const createSchema = createInsertSchema(registry, { registryName: z.string().min(1), username: z.string().min(1), password: z.string().min(1), - registryUrl: z.string(), + registryUrl: registryUrlSchema, organizationId: z.string().min(1), registryId: z.string().min(1), registryType: z.enum(["cloud"]), @@ -61,7 +72,7 @@ export const apiCreateRegistry = createSchema registryName: z.string().min(1), username: z.string().min(1), password: z.string().min(1), - registryUrl: z.string(), + registryUrl: registryUrlSchema, registryType: z.enum(["cloud"]), imagePrefix: z.string().nullable().optional(), }) @@ -74,7 +85,7 @@ export const apiTestRegistry = createSchema.pick({}).extend({ registryName: z.string().optional(), username: z.string().min(1), password: z.string().min(1), - registryUrl: z.string(), + registryUrl: registryUrlSchema, registryType: z.enum(["cloud"]), imagePrefix: z.string().nullable().optional(), serverId: z.string().optional(),
packages/server/src/services/registry.ts+1 −1 modified@@ -85,7 +85,7 @@ export const removeRegistry = async (registryId: string) => { } if (!IS_CLOUD) { - await execAsync(`docker logout ${response.registryUrl}`); + await execAsync(`docker logout ${shEscape(response.registryUrl)}`); } return response;
fef2de1ec587feat(validation): add branch name validation across provider schemas
13 files changed · +38 −15
apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx+2 −1 modified@@ -5,6 +5,7 @@ import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { BitbucketIcon } from "@/components/icons/data-tools-icons"; import { AlertBlock } from "@/components/shared/alert-block"; import { Badge } from "@/components/ui/badge"; @@ -57,7 +58,7 @@ const BitbucketProviderSchema = z.object({ slug: z.string().optional(), }) .required(), - branch: z.string().min(1, "Branch is required"), + branch: z.string().min(1, "Branch is required").regex(VALID_BRANCH_REGEX, "Invalid branch name"), bitbucketId: z.string().min(1, "Bitbucket Provider is required"), watchPaths: z.array(z.string()).optional(), enableSubmodules: z.boolean().optional(),
apps/dokploy/components/dashboard/application/general/generic/save-gitea-provider.tsx+2 −1 modified@@ -5,6 +5,7 @@ import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { GiteaIcon } from "@/components/icons/data-tools-icons"; import { AlertBlock } from "@/components/shared/alert-block"; import { Badge } from "@/components/ui/badge"; @@ -72,7 +73,7 @@ const GiteaProviderSchema = z.object({ owner: z.string().min(1, "Owner is required"), }) .required(), - branch: z.string().min(1, "Branch is required"), + branch: z.string().min(1, "Branch is required").regex(VALID_BRANCH_REGEX, "Invalid branch name"), giteaId: z.string().min(1, "Gitea Provider is required"), watchPaths: z.array(z.string()).default([]), enableSubmodules: z.boolean().optional(),
apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx+2 −1 modified@@ -5,6 +5,7 @@ import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { GithubIcon } from "@/components/icons/data-tools-icons"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -55,7 +56,7 @@ const GithubProviderSchema = z.object({ owner: z.string().min(1, "Owner is required"), }) .required(), - branch: z.string().min(1, "Branch is required"), + branch: z.string().min(1, "Branch is required").regex(VALID_BRANCH_REGEX, "Invalid branch name"), githubId: z.string().min(1, "Github Provider is required"), watchPaths: z.array(z.string()).optional(), triggerType: z.enum(["push", "tag"]).default("push"),
apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx+2 −1 modified@@ -5,6 +5,7 @@ import { useEffect, useMemo } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { GitlabIcon } from "@/components/icons/data-tools-icons"; import { AlertBlock } from "@/components/shared/alert-block"; import { Badge } from "@/components/ui/badge"; @@ -58,7 +59,7 @@ const GitlabProviderSchema = z.object({ id: z.number().nullable(), }) .required(), - branch: z.string().min(1, "Branch is required"), + branch: z.string().min(1, "Branch is required").regex(VALID_BRANCH_REGEX, "Invalid branch name"), gitlabId: z.string().min(1, "Gitlab Provider is required"), watchPaths: z.array(z.string()).optional(), enableSubmodules: z.boolean().default(false),
apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx+2 −1 modified@@ -6,6 +6,7 @@ import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { GitIcon } from "@/components/icons/data-tools-icons"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -41,7 +42,7 @@ const GitProviderSchema = z.object({ repositoryURL: z.string().min(1, { message: "Repository URL is required", }), - branch: z.string().min(1, "Branch required"), + branch: z.string().min(1, "Branch required").regex(VALID_BRANCH_REGEX, "Invalid branch name"), sshKey: z.string().optional(), watchPaths: z.array(z.string()).optional(), enableSubmodules: z.boolean().default(false),
apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx+2 −1 modified@@ -5,6 +5,7 @@ import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { BitbucketIcon } from "@/components/icons/data-tools-icons"; import { AlertBlock } from "@/components/shared/alert-block"; import { Badge } from "@/components/ui/badge"; @@ -57,7 +58,7 @@ const BitbucketProviderSchema = z.object({ slug: z.string().optional(), }) .required(), - branch: z.string().min(1, "Branch is required"), + branch: z.string().min(1, "Branch is required").regex(VALID_BRANCH_REGEX, "Invalid branch name"), bitbucketId: z.string().min(1, "Bitbucket Provider is required"), watchPaths: z.array(z.string()).optional(), enableSubmodules: z.boolean().default(false),
apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx+2 −1 modified@@ -5,6 +5,7 @@ import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { GiteaIcon } from "@/components/icons/data-tools-icons"; import { AlertBlock } from "@/components/shared/alert-block"; import { Badge } from "@/components/ui/badge"; @@ -57,7 +58,7 @@ const GiteaProviderSchema = z.object({ owner: z.string().min(1, "Owner is required"), }) .required(), - branch: z.string().min(1, "Branch is required"), + branch: z.string().min(1, "Branch is required").regex(VALID_BRANCH_REGEX, "Invalid branch name"), giteaId: z.string().min(1, "Gitea Provider is required"), watchPaths: z.array(z.string()).optional(), enableSubmodules: z.boolean().default(false),
apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx+5 −1 modified@@ -1,3 +1,4 @@ +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; import { CheckIcon, ChevronsUpDown, HelpCircle, X } from "lucide-react"; import Link from "next/link"; @@ -55,7 +56,10 @@ const GithubProviderSchema = z.object({ owner: z.string().min(1, "Owner is required"), }) .required(), - branch: z.string().min(1, "Branch is required"), + branch: z + .string() + .min(1, "Branch is required") + .regex(VALID_BRANCH_REGEX, "Invalid branch name"), githubId: z.string().min(1, "Github Provider is required"), watchPaths: z.array(z.string()).optional(), triggerType: z.enum(["push", "tag"]).default("push"),
apps/dokploy/components/dashboard/compose/general/generic/save-gitlab-provider-compose.tsx+2 −1 modified@@ -5,6 +5,7 @@ import { useEffect, useMemo } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { GitlabIcon } from "@/components/icons/data-tools-icons"; import { AlertBlock } from "@/components/shared/alert-block"; import { Badge } from "@/components/ui/badge"; @@ -58,7 +59,7 @@ const GitlabProviderSchema = z.object({ gitlabPathNamespace: z.string().min(1), }) .required(), - branch: z.string().min(1, "Branch is required"), + branch: z.string().min(1, "Branch is required").regex(VALID_BRANCH_REGEX, "Invalid branch name"), gitlabId: z.string().min(1, "Gitlab Provider is required"), watchPaths: z.array(z.string()).optional(), enableSubmodules: z.boolean().default(false),
apps/dokploy/components/dashboard/compose/general/generic/save-git-provider-compose.tsx+2 −1 modified@@ -6,6 +6,7 @@ import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { GitIcon } from "@/components/icons/data-tools-icons"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -41,7 +42,7 @@ const GitProviderSchema = z.object({ repositoryURL: z.string().min(1, { message: "Repository URL is required", }), - branch: z.string().min(1, "Branch required"), + branch: z.string().min(1, "Branch required").regex(VALID_BRANCH_REGEX, "Invalid branch name"), sshKey: z.string().optional(), watchPaths: z.array(z.string()).optional(), enableSubmodules: z.boolean().default(false),
packages/server/src/db/schema/application.ts+11 −5 modified@@ -1,3 +1,4 @@ +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { relations } from "drizzle-orm"; import { bigint, @@ -432,17 +433,22 @@ export const apiSaveBuildType = createSchema .required() .merge(createSchema.pick({ publishDirectory: true, isStaticSpa: true })); +const branchField = z + .string() + .min(1) + .regex(VALID_BRANCH_REGEX, "Invalid branch name"); + export const apiSaveGithubProvider = createSchema .pick({ applicationId: true, repository: true, - branch: true, owner: true, buildPath: true, githubId: true, }) .required() .extend({ + branch: branchField, triggerType: z.enum(["push", "tag"]).default("push"), }) .required() @@ -451,7 +457,6 @@ export const apiSaveGithubProvider = createSchema export const apiSaveGitlabProvider = createSchema .pick({ applicationId: true, - gitlabBranch: true, gitlabBuildPath: true, gitlabOwner: true, gitlabRepository: true, @@ -460,11 +465,11 @@ export const apiSaveGitlabProvider = createSchema gitlabPathNamespace: true, }) .required() + .extend({ gitlabBranch: branchField }) .merge(createSchema.pick({ enableSubmodules: true, watchPaths: true })); export const apiSaveBitbucketProvider = createSchema .pick({ - bitbucketBranch: true, bitbucketBuildPath: true, bitbucketOwner: true, bitbucketRepository: true, @@ -473,18 +478,19 @@ export const apiSaveBitbucketProvider = createSchema applicationId: true, }) .required() + .extend({ bitbucketBranch: branchField }) .merge(createSchema.pick({ enableSubmodules: true, watchPaths: true })); export const apiSaveGiteaProvider = createSchema .pick({ applicationId: true, - giteaBranch: true, giteaBuildPath: true, giteaOwner: true, giteaRepository: true, giteaId: true, }) .required() + .extend({ giteaBranch: branchField }) .merge(createSchema.pick({ enableSubmodules: true, watchPaths: true })); export const apiSaveDockerProvider = createSchema @@ -499,14 +505,14 @@ export const apiSaveDockerProvider = createSchema export const apiSaveGitProvider = createSchema .pick({ - customGitBranch: true, applicationId: true, customGitBuildPath: true, customGitUrl: true, watchPaths: true, enableSubmodules: true, }) .required() + .extend({ customGitBranch: branchField }) .merge( createSchema.pick({ customGitSSHKeyId: true,
packages/server/src/index.ts+1 −0 modified@@ -108,6 +108,7 @@ export * from "./utils/notifications/docker-cleanup"; export * from "./utils/notifications/dokploy-restart"; export * from "./utils/notifications/server-threshold"; export * from "./utils/notifications/utils"; +export * from "./utils/git-branch-validation"; export * from "./utils/process/execAsync"; export * from "./utils/process/spawnAsync"; export * from "./utils/providers/bitbucket";
packages/server/src/utils/git-branch-validation.ts+3 −0 added@@ -0,0 +1,3 @@ +// Valid git branch names per git-check-ref-format rules. +// Rejects shell metacharacters that would enable command injection. +export const VALID_BRANCH_REGEX = /^[a-zA-Z0-9._\-/]+$/;
b20ff64cbf37chore(package): bump version to v0.29.3
1 file changed · +1 −1
apps/dokploy/package.json+1 −1 modified@@ -1,6 +1,6 @@ { "name": "dokploy", - "version": "v0.29.2", + "version": "v0.29.3", "private": true, "license": "Apache-2.0", "type": "module",
34a6d9ff12c3b9e97eb32135feat(validation): enhance destination path validation in file upload schema
2 files changed · +13 −2
apps/dokploy/utils/schema.ts+7 −1 modified@@ -28,7 +28,13 @@ export const uploadFileToContainerSchema = zfd.formData({ .min(1) .regex(/^[a-zA-Z0-9.\-_]+$/, "Invalid container ID"), file: zfd.file(), - destinationPath: z.string().min(1), + destinationPath: z + .string() + .min(1) + .regex( + /^[a-zA-Z0-9.\-_/]+$/, + "Invalid destination path: only alphanumeric characters, dots, dashes, underscores, and forward slashes are allowed", + ), serverId: z.string().optional(), });
packages/server/src/services/docker.ts+6 −1 modified@@ -655,6 +655,8 @@ export const getAllContainerStats = async (serverId?: string) => { } }; +const destinationPathRegex = /^[a-zA-Z0-9.\-_/]+$/; + export const uploadFileToContainer = async ( containerId: string, fileBuffer: Buffer, @@ -667,7 +669,10 @@ export const uploadFileToContainer = async ( throw new Error("Invalid container ID"); } - // Ensure destination path starts with / + if (!destinationPathRegex.test(destinationPath)) { + throw new Error("Invalid destination path: shell metacharacters are not allowed"); + } + const normalizedPath = destinationPath.startsWith("/") ? destinationPath : `/${destinationPath}`;
a4e2317f3e06feat(deployment): enhance log retrieval by encoding log path in base64
2 files changed · +8 −3
apps/dokploy/server/wss/listen-deployment.ts+4 −3 modified@@ -1,6 +1,7 @@ import { spawn } from "node:child_process"; import type http from "node:http"; import { findServerById, IS_CLOUD, validateRequest } from "@dokploy/server"; +import { encodeBase64 } from "@dokploy/server/utils/docker/utils"; import { readValidDirectory } from "@dokploy/server/wss/utils"; import { Client } from "ssh2"; import { WebSocketServer } from "ws"; @@ -70,9 +71,9 @@ export const setupDeploymentLogsWebSocketServer = ( sshClient = new Client(); sshClient .on("ready", () => { - const command = ` - tail -n +1 -f ${logPath}; - `; + const encodedPath = encodeBase64(logPath); + const command = `tail -n +1 -f "$(echo '${encodedPath}' | base64 -d)"`; + sshClient!.exec(command, (err, stream) => { if (err) { sshClient!.end();
packages/server/src/wss/utils.ts+4 −0 modified@@ -40,6 +40,10 @@ export const readValidDirectory = ( directory: string, serverId?: string | null, ) => { + if (!/^[\w/. -]{1,500}$/.test(directory)) { + return false; + } + const { BASE_PATH } = paths(!!serverId); const resolvedBase = path.resolve(BASE_PATH);
06a349152f20fix(traefik): update remote config writing to use base64 encoding
1 file changed · +5 −1
packages/server/src/utils/traefik/application.ts+5 −1 modified@@ -218,7 +218,11 @@ export const writeConfigRemote = async ( try { const { DYNAMIC_TRAEFIK_PATH } = paths(true); const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`); - await execAsyncRemote(serverId, `echo '${traefikConfig}' > ${configPath}`); + const encoded = encodeBase64(traefikConfig); + await execAsyncRemote( + serverId, + `echo "${encoded}" | base64 -d > "${configPath}"`, + ); } catch (e) { console.error("Error saving the YAML config file:", e); }
62aeed5aedd7fix(esbuild): update path for migrate-auth-secret script
1 file changed · +1 −1
apps/dokploy/esbuild.config.ts+1 −1 modified@@ -28,7 +28,7 @@ try { "wait-for-postgres": "wait-for-postgres.ts", "reset-password": "reset-password.ts", "reset-2fa": "reset-2fa.ts", - "migrate-auth-secret": "migrate-auth-secret.ts", + "migrate-auth-secret": "scripts/migrate-auth-secret.ts", }, bundle: true, platform: "node",
Vulnerability mechanics
Root cause
"User-supplied branch names, repository URLs, and Docker credentials are interpolated into shell commands via JavaScript template literals without escaping, and executed through /bin/sh -c."
Attack vector
An authenticated attacker with application create/edit privileges sets the branch field (or repository URL / Docker password) to a value containing shell metacharacters such as `;`, `|`, or `` ` ``. When the user triggers a deployment, Dokploy constructs a shell command like `git clone --branch ${branch} ...` and executes it via `child_process.exec()` (which invokes `/bin/sh -c`). The injected metacharacters break out of the intended argument and allow arbitrary command execution on the Dokploy server. Because Dokploy mounts the Docker socket, the attacker can escalate to full host compromise. The webhook deploy endpoint does not expose this to unauthenticated attackers because its branch-matching logic requires the injected payload to match exactly, which no real git provider would send. [ref_id=1]
Affected code
The vulnerability exists in multiple provider files under `packages/server/src/utils/providers/` (`git.ts`, `github.ts`, `gitlab.ts`, `bitbucket.ts`, `docker.ts`) where user-supplied branch names, repository URLs, and Docker credentials are interpolated into shell commands via JavaScript template literals without escaping. The `execAsync` function (wrapping `child_process.exec`) runs these commands through `/bin/sh -c`, making every interpolation point a command injection vector. The codebase already had a `shEscape()` helper in `packages/server/src/services/registry.ts` but it was never applied to the deployment pipeline paths. [ref_id=1]
What the fix does
The patches address the root cause by adding input validation at multiple layers. [patch_id=3104647] introduces a `VALID_BRANCH_REGEX` and applies it to branch fields in all provider schemas (GitHub, GitLab, Bitbucket, Gitea, and custom Git), rejecting names containing shell metacharacters. [patch_id=3104643] adds a strict regex for registry URLs (hostname:port only) and escapes the `registryUrl` with `shEscape()` in the `removeRegistry` function. [patch_id=3104642] and [patch_id=3104645] switch to base64-encoding of configuration and log paths before passing them to shell commands, preventing injection through those vectors. [patch_id=3104646] adds a regex whitelist for destination paths in file uploads. Together these changes ensure that user-supplied values cannot break out of their intended shell context.
Preconditions
- authAttacker must be authenticated to Dokploy
- authAttacker must have application create or edit privileges
- inputAttacker must supply a branch name, repository URL, or Docker credential containing shell metacharacters
Generated on May 29, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
1News mentions
0No linked articles in our index yet.