VYPR
Critical severity9.6NVD Advisory· Published May 29, 2026· Updated May 29, 2026

CVE-2026-45628

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

2
  • Dokploy/Dokployinferred2 versions
    <=0.29.2+ 1 more
    • (no CPE)range: <=0.29.2
    • (no CPE)range: <=0.29.2

Patches

9
5e021797f3cd

feat(validation): standardize branch name validation across provider schemas

https://github.com/Dokploy/dokployMauricio SiuMay 11, 2026Fixed in 0.29.3via llm-release-walk
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",
    
547ba2d04bc6

feat(validation): enhance registry URL validation in schema

https://github.com/Dokploy/dokployMauricio SiuMay 9, 2026Fixed in 0.29.3via llm-release-walk
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;
    
fef2de1ec587

feat(validation): add branch name validation across provider schemas

https://github.com/Dokploy/dokployMauricio SiuMay 9, 2026Fixed in 0.29.3via llm-release-walk
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._\-/]+$/;
    
b20ff64cbf37

chore(package): bump version to v0.29.3

https://github.com/Dokploy/dokployMauricio SiuMay 9, 2026Fixed in 0.29.3via llm-release-walk
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",
    
34a6d9ff12c3
https://github.com/Dokploy/dokployFixed in 0.29.3via llm-release-walk
b9e97eb32135

feat(validation): enhance destination path validation in file upload schema

https://github.com/Dokploy/dokployMauricio SiuMay 9, 2026Fixed in 0.29.3via llm-release-walk
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}`;
    
a4e2317f3e06

feat(deployment): enhance log retrieval by encoding log path in base64

https://github.com/Dokploy/dokployMauricio SiuMay 9, 2026Fixed in 0.29.3via llm-release-walk
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);
    
06a349152f20

fix(traefik): update remote config writing to use base64 encoding

https://github.com/Dokploy/dokployMauricio SiuMay 9, 2026Fixed in 0.29.3via llm-release-walk
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);
     	}
    
62aeed5aedd7

fix(esbuild): update path for migrate-auth-secret script

https://github.com/Dokploy/dokployMauricio SiuMay 11, 2026Fixed in 0.29.3via release-tag
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

1

News mentions

0

No linked articles in our index yet.