VYPR
Critical severity9.9NVD Advisory· Published May 18, 2026· Updated May 19, 2026

CVE-2026-27130

CVE-2026-27130

Description

Dokploy is a free, self-hostable Platform as a Service (PaaS). Versions 0.26.6 and below have OS command injection through the appName parameter. 3 chained issues cause this problem: inadequate input sanitization, lack of schema validation and direct shell interpolation. User-controlled application names are passed through inadequate sanitization (cleanAppName function only replaces spaces and converts to lowercase) before being interpolated directly into shell commands executed via execAsync() and execAsyncRemote(). An authenticated attacker can inject shell metacharacters (e.g., ;, $(), backticks, |, &) in the appName field during application creation, which are then executed with server-level privileges when service operations (start, stop, remove, scale) are triggered. This issue has been resolved in version 0.26.7.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

OS command injection in Dokploy <=0.26.6 via appName parameter allows authenticated RCE with server-level privileges.

Vulnerability

Dokploy versions 0.26.6 and below have an OS command injection vulnerability in the application creation endpoint. The appName parameter is inadequately sanitized by the cleanAppName() function, which only replaces spaces and converts to lowercase, failing to remove shell metacharacters [2]. There is no schema validation for appName in the API schema [2]. User-controlled application names are stored and later interpolated directly into shell commands executed via execAsync() and execAsyncRemote() during service operations like start, stop, remove, and scale [2]. This issue is fixed in version 0.26.7 [1].

Exploitation

An authenticated attacker can inject shell metacharacters (e.g., ;, $(), backticks, |, &) in the appName field during application creation [2]. The injected payload is executed with server-level privileges when any service operation (start, stop, remove, scale) is triggered [2]. No additional user interaction is required beyond authentication and submitting the malicious app name.

Impact

Successful exploitation allows an authenticated attacker to execute arbitrary OS commands on the Dokploy server, leading to full server compromise. The impact includes disclosure of sensitive data, modification or deletion of files, and potential lateral movement within the infrastructure [2].

Mitigation

The vulnerability has been resolved in Dokploy version 0.26.7 [1][2]. The fix adds proper schema validation with a regex (APP_NAME_REGEX) and input length limits across database schemas [1]. Upgrading to 0.26.7 or later is the recommended mitigation.

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2
  • Dokploy/Dokployreferences2 versions
    (expand)+ 1 more
    • (no CPE)
    • (no CPE)range: <=0.26.6

Patches

1
960892fd8dcf

feat(schema): enhance appName validation across database schemas with regex and message

https://github.com/Dokploy/dokployMauricio SiuJan 31, 2026via nvd-ref
8 files changed · +110 77
  • packages/server/src/db/schema/application.ts+7 2 modified
    @@ -47,7 +47,7 @@ import {
     	UpdateConfigSwarmSchema,
     } from "./shared";
     import { sshKeys } from "./ssh-key";
    -import { generateAppName } from "./utils";
    +import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils";
     export const sourceType = pgEnum("sourceType", [
     	"docker",
     	"git",
    @@ -287,7 +287,12 @@ export const applicationsRelations = relations(
     );
     
     const createSchema = createInsertSchema(applications, {
    -	appName: z.string(),
    +	appName: z
    +		.string()
    +		.min(1)
    +		.max(63)
    +		.regex(APP_NAME_REGEX, APP_NAME_MESSAGE)
    +		.optional(),
     	createdAt: z.string(),
     	applicationId: z.string(),
     	autoDeploy: z.boolean(),
    
  • packages/server/src/db/schema/compose.ts+7 1 modified
    @@ -16,7 +16,7 @@ import { schedules } from "./schedule";
     import { server } from "./server";
     import { applicationStatus, triggerType } from "./shared";
     import { sshKeys } from "./ssh-key";
    -import { generateAppName } from "./utils";
    +import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils";
     export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
     	"git",
     	"github",
    @@ -147,6 +147,12 @@ export const composeRelations = relations(compose, ({ one, many }) => ({
     
     const createSchema = createInsertSchema(compose, {
     	name: z.string().min(1),
    +	appName: z
    +		.string()
    +		.min(1)
    +		.max(63)
    +		.regex(APP_NAME_REGEX, APP_NAME_MESSAGE)
    +		.optional(),
     	description: z.string(),
     	env: z.string().optional(),
     	composeFile: z.string().optional(),
    
  • packages/server/src/db/schema/mariadb.ts+19 16 modified
    @@ -26,7 +26,7 @@ import {
     	type UpdateConfigSwarm,
     	UpdateConfigSwarmSchema,
     } from "./shared";
    -import { generateAppName } from "./utils";
    +import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils";
     
     export const mariadb = pgTable("mariadb", {
     	mariadbId: text("mariadbId")
    @@ -96,7 +96,12 @@ export const mariadbRelations = relations(mariadb, ({ one, many }) => ({
     const createSchema = createInsertSchema(mariadb, {
     	mariadbId: z.string(),
     	name: z.string().min(1),
    -	appName: z.string().min(1),
    +	appName: z
    +		.string()
    +		.min(1)
    +		.max(63)
    +		.regex(APP_NAME_REGEX, APP_NAME_MESSAGE)
    +		.optional(),
     	createdAt: z.string(),
     	databaseName: z.string().min(1),
     	databaseUser: z.string().min(1),
    @@ -138,20 +143,18 @@ const createSchema = createInsertSchema(mariadb, {
     	endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
     });
     
    -export const apiCreateMariaDB = createSchema
    -	.pick({
    -		name: true,
    -		appName: true,
    -		dockerImage: true,
    -		databaseRootPassword: true,
    -		environmentId: true,
    -		description: true,
    -		databaseName: true,
    -		databaseUser: true,
    -		databasePassword: true,
    -		serverId: true,
    -	})
    -	.required();
    +export const apiCreateMariaDB = createSchema.pick({
    +	name: true,
    +	appName: true,
    +	dockerImage: true,
    +	databaseRootPassword: true,
    +	environmentId: true,
    +	description: true,
    +	databaseName: true,
    +	databaseUser: true,
    +	databasePassword: true,
    +	serverId: true,
    +});
     
     export const apiFindOneMariaDB = createSchema
     	.pick({
    
  • packages/server/src/db/schema/mongo.ts+18 15 modified
    @@ -33,7 +33,7 @@ import {
     	type UpdateConfigSwarm,
     	UpdateConfigSwarmSchema,
     } from "./shared";
    -import { generateAppName } from "./utils";
    +import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils";
     
     export const mongo = pgTable("mongo", {
     	mongoId: text("mongoId")
    @@ -98,7 +98,12 @@ export const mongoRelations = relations(mongo, ({ one, many }) => ({
     }));
     
     const createSchema = createInsertSchema(mongo, {
    -	appName: z.string().min(1),
    +	appName: z
    +		.string()
    +		.min(1)
    +		.max(63)
    +		.regex(APP_NAME_REGEX, APP_NAME_MESSAGE)
    +		.optional(),
     	createdAt: z.string(),
     	mongoId: z.string(),
     	name: z.string().min(1),
    @@ -135,19 +140,17 @@ const createSchema = createInsertSchema(mongo, {
     	endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
     });
     
    -export const apiCreateMongo = createSchema
    -	.pick({
    -		name: true,
    -		appName: true,
    -		dockerImage: true,
    -		environmentId: true,
    -		description: true,
    -		databaseUser: true,
    -		databasePassword: true,
    -		serverId: true,
    -		replicaSets: true,
    -	})
    -	.required();
    +export const apiCreateMongo = createSchema.pick({
    +	name: true,
    +	appName: true,
    +	dockerImage: true,
    +	environmentId: true,
    +	description: true,
    +	databaseUser: true,
    +	databasePassword: true,
    +	serverId: true,
    +	replicaSets: true,
    +});
     
     export const apiFindOneMongo = createSchema
     	.pick({
    
  • packages/server/src/db/schema/mysql.ts+19 16 modified
    @@ -26,7 +26,7 @@ import {
     	type UpdateConfigSwarm,
     	UpdateConfigSwarmSchema,
     } from "./shared";
    -import { generateAppName } from "./utils";
    +import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils";
     
     export const mysql = pgTable("mysql", {
     	mysqlId: text("mysqlId")
    @@ -93,7 +93,12 @@ export const mysqlRelations = relations(mysql, ({ one, many }) => ({
     
     const createSchema = createInsertSchema(mysql, {
     	mysqlId: z.string(),
    -	appName: z.string().min(1),
    +	appName: z
    +		.string()
    +		.min(1)
    +		.max(63)
    +		.regex(APP_NAME_REGEX, APP_NAME_MESSAGE)
    +		.optional(),
     	createdAt: z.string(),
     	name: z.string().min(1),
     	databaseName: z.string().min(1),
    @@ -135,20 +140,18 @@ const createSchema = createInsertSchema(mysql, {
     	endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
     });
     
    -export const apiCreateMySql = createSchema
    -	.pick({
    -		name: true,
    -		appName: true,
    -		dockerImage: true,
    -		environmentId: true,
    -		description: true,
    -		databaseName: true,
    -		databaseUser: true,
    -		databasePassword: true,
    -		databaseRootPassword: true,
    -		serverId: true,
    -	})
    -	.required();
    +export const apiCreateMySql = createSchema.pick({
    +	name: true,
    +	appName: true,
    +	dockerImage: true,
    +	environmentId: true,
    +	description: true,
    +	databaseName: true,
    +	databaseUser: true,
    +	databasePassword: true,
    +	databaseRootPassword: true,
    +	serverId: true,
    +});
     
     export const apiFindOneMySql = createSchema
     	.pick({
    
  • packages/server/src/db/schema/postgres.ts+18 14 modified
    @@ -26,7 +26,7 @@ import {
     	type UpdateConfigSwarm,
     	UpdateConfigSwarmSchema,
     } from "./shared";
    -import { generateAppName } from "./utils";
    +import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils";
     
     export const postgres = pgTable("postgres", {
     	postgresId: text("postgresId")
    @@ -94,6 +94,12 @@ export const postgresRelations = relations(postgres, ({ one, many }) => ({
     const createSchema = createInsertSchema(postgres, {
     	postgresId: z.string(),
     	name: z.string().min(1),
    +	appName: z
    +		.string()
    +		.min(1)
    +		.max(63)
    +		.regex(APP_NAME_REGEX, APP_NAME_MESSAGE)
    +		.optional(),
     	databasePassword: z
     		.string()
     		.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
    @@ -128,19 +134,17 @@ const createSchema = createInsertSchema(postgres, {
     	endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
     });
     
    -export const apiCreatePostgres = createSchema
    -	.pick({
    -		name: true,
    -		appName: true,
    -		databaseName: true,
    -		databaseUser: true,
    -		databasePassword: true,
    -		dockerImage: true,
    -		environmentId: true,
    -		description: true,
    -		serverId: true,
    -	})
    -	.required();
    +export const apiCreatePostgres = createSchema.pick({
    +	name: true,
    +	appName: true,
    +	databaseName: true,
    +	databaseUser: true,
    +	databasePassword: true,
    +	dockerImage: true,
    +	environmentId: true,
    +	description: true,
    +	serverId: true,
    +});
     
     export const apiFindOnePostgres = createSchema
     	.pick({
    
  • packages/server/src/db/schema/redis.ts+16 13 modified
    @@ -25,7 +25,7 @@ import {
     	type UpdateConfigSwarm,
     	UpdateConfigSwarmSchema,
     } from "./shared";
    -import { generateAppName } from "./utils";
    +import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils";
     
     export const redis = pgTable("redis", {
     	redisId: text("redisId")
    @@ -88,7 +88,12 @@ export const redisRelations = relations(redis, ({ one, many }) => ({
     
     const createSchema = createInsertSchema(redis, {
     	redisId: z.string(),
    -	appName: z.string().min(1),
    +	appName: z
    +		.string()
    +		.min(1)
    +		.max(63)
    +		.regex(APP_NAME_REGEX, APP_NAME_MESSAGE)
    +		.optional(),
     	createdAt: z.string(),
     	name: z.string().min(1),
     	databasePassword: z.string(),
    @@ -117,17 +122,15 @@ const createSchema = createInsertSchema(redis, {
     	endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
     });
     
    -export const apiCreateRedis = createSchema
    -	.pick({
    -		name: true,
    -		appName: true,
    -		databasePassword: true,
    -		dockerImage: true,
    -		environmentId: true,
    -		description: true,
    -		serverId: true,
    -	})
    -	.required();
    +export const apiCreateRedis = createSchema.pick({
    +	name: true,
    +	appName: true,
    +	databasePassword: true,
    +	dockerImage: true,
    +	environmentId: true,
    +	description: true,
    +	serverId: true,
    +});
     
     export const apiFindOneRedis = createSchema
     	.pick({
    
  • packages/server/src/db/schema/utils.ts+6 0 modified
    @@ -6,6 +6,12 @@ const alphabet = "abcdefghijklmnopqrstuvwxyz123456789";
     
     const customNanoid = customAlphabet(alphabet, 6);
     
    +/** App name: letters, numbers, dots, underscores, hyphens only (no spaces). Safe for shell/Docker. */
    +export const APP_NAME_REGEX = /^[a-zA-Z0-9._-]+$/;
    +
    +export const APP_NAME_MESSAGE =
    +	"App name can only contain letters, numbers, dots, underscores and hyphens";
    +
     export const generateAppName = (type: string) => {
     	const verb = faker.hacker.verb().replace(/ /g, "-");
     	const adjective = faker.hacker.adjective().replace(/ /g, "-");
    

Vulnerability mechanics

Root cause

"User-controlled appName values are interpolated directly into shell commands without proper validation, allowing shell metacharacters to be injected."

Attack vector

An authenticated attacker provides a malicious appName (e.g., containing `;`, `$()`, or backticks) when creating an application or database service. The `cleanAppName` function only replaces spaces and lowercases the string, leaving shell metacharacters intact. When service operations such as start, stop, remove, or scale are triggered, the unsanitized appName is interpolated into shell commands executed via `execAsync()` or `execAsyncRemote()`, resulting in OS command injection with server-level privileges [CWE-78].

Affected code

The vulnerability spans multiple schema files under `packages/server/src/db/schema/`: `application.ts`, `compose.ts`, `mariadb.ts`, `mysql.ts`, `mongo.ts`, `postgres.ts`, and `redis.ts`. The `cleanAppName` utility function (referenced in the advisory) only replaces spaces and converts to lowercase, failing to remove shell metacharacters. The `execAsync()` and `execAsyncRemote()` functions (not shown in the patch) execute shell commands that interpolate the unsanitized appName.

What the fix does

The patch adds a strict regex validation (`APP_NAME_REGEX = /^[a-zA-Z0-9._-]+$/`) to the `appName` field in every database schema (mariadb, mysql, mongo, postgres, redis) and in the application and compose schemas [patch_id=424400]. This restricts appName to only letters, numbers, dots, underscores, and hyphens, which are safe for shell interpolation. The validation is applied via Zod's `.regex()` at the schema level, so any input containing shell metacharacters is rejected before it reaches any command execution code. The patch also adds a maximum length constraint (63 characters) and a descriptive error message.

Preconditions

  • authAttacker must be authenticated to the Dokploy instance
  • inputAttacker must have access to create or modify an application/database service with a controlled appName field
  • configThe vulnerable cleanAppName function must be the only sanitization applied before shell execution

Generated on May 19, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

2

News mentions

0

No linked articles in our index yet.