{"repro_id":"REPRO-2026-00245","version":6,"title":"Coolify authenticated command injection in Destination Network Management","repro_type":"security","status":"published","severity":"high","description":"Authenticated command injection in Coolify Destination Network Management (CVE-2026-34594, GHSA-mf8p-rj62-9f9m). The 'network' parameter from user input is interpolated directly into shell commands (docker network disconnect / docker network rm) without escapeshellarg(), allowing a user with destination management permissions to execute arbitrary commands as root on the Coolify host.","root_cause":"# CVE-2026-34594 — Root Cause Analysis\n\n## Summary\n\nCoolify (self-hosted PaaS, Laravel/Livewire application) before `4.0.0-beta.471`\ncontains an authenticated command-injection vulnerability in the **Destination\nNetwork Management** functionality. The `network` field of a Docker Destination\n(`App\\Livewire\\Destination\\New\\Docker` and `App\\Livewire\\Destination\\Show`) was\nonly validated as `['required','string']`, so an authenticated user with\ndestination-management permissions could submit a value containing shell\nmetacharacters. That value is stored on the `StandaloneDocker` / `SwarmDocker`\nmodel and later interpolated **unsanitized** into shell commands that Coolify\nexecutes on the destination's server over SSH\n(`instant_remote_process()` → `SshMultiplexingHelper::generateSshCommand()` →\n`ssh ... 'bash -se' << $delimiter`). Because the network string becomes the\nbody of a here-doc fed to `bash -se` on the remote host, an attacker-controlled\n`network` such as `x;id > /tmp/pwned #` results in arbitrary command execution\n**as root** on the Coolify-managed server at the moment the destination is\ncreated (the `StandaloneDocker::created` boot event) and again when it is\ndeleted.\n\n## Impact\n\n- **Package/component affected:** `coollabsio/coolify` — Destination Network\n  Management (`app/Livewire/Destination/New/Docker.php`,\n  `app/Livewire/Destination/Show.php`, `app/Models/StandaloneDocker.php`,\n  `app/Models/SwarmDocker.php`, and every shell consumer of\n  `->destination->network`: `ApplicationDeploymentJob`, `DatabaseBackupJob`,\n  `StartService`, `Init` command, `bootstrap/helpers/proxy.php`).\n- **Affected versions:** Coolify `< 4.0.0-beta.471` (verified on the official\n  image `ghcr.io/coollabsio/coolify:4.0.0-beta.470`).\n- **Risk level:** High. Coolify executes SSH commands with the destination\n  server's user (default `root`). The injected command runs with that\n  privilege on the managed host, enabling full server compromise, access to the\n  Docker socket, lateral movement, and exposure of all managed resources /\n  secrets. Exploitation requires only an authenticated user who can create a\n  destination (the `StandaloneDockerPolicy::create` check returns `true` for any\n  authenticated user; `createAnyResource` likewise returns `true`).\n\n## Impact Parity\n\n- **Disclosed/claimed maximum impact:** Authenticated command execution\n  (\"arbitrary command execution on the Coolify host\") via the Destination\n  Network Management network field.\n- **Reproduced impact from this run:** Authenticated command execution as\n  `root` on the destination server. The injected `id` command wrote\n  `uid=0(root) gid=0(root) groups=0(root)` to a marker file on the managed\n  server, proving arbitrary command execution with root privileges through the\n  authenticated web request.\n- **Parity:** `full` — the claimed authenticated command injection was\n  demonstrated end-to-end against the real product, and the fixed build\n  rejects the same input with no execution.\n\n## Root Cause\n\nThe `network` attribute of a Docker Destination is attacker-controlled input\nthat flows into privileged shell commands without validation or escaping.\n\nVulnerable code path (beta.470):\n\n1. `app/Livewire/Destination/New/Docker.php` declares the field with only\n   `#[Validate(['required','string'])]` (no character whitelist), and `submit()`\n   calls `StandaloneDocker::create(['network' => $this->network, ...])`.\n2. `app/Models/StandaloneDocker.php` `boot()` registers a `static::created`\n   hook that runs:\n   ```php\n   instant_remote_process([\n       \"docker network inspect $newStandaloneDocker->network >/dev/null 2>&1 || docker network create --driver overlay --attachable $newStandaloneDocker->network >/dev/null\",\n   ], $server, false);\n   ```\n   The network is interpolated directly into the command string.\n3. `instant_remote_process()` builds an SSH command via\n   `SshMultiplexingHelper::generateSshCommand()`, which sends the command as the\n   body of a here-doc to `bash -se` on the remote server:\n   ```\n   timeout <t> ssh ... 'root'@'<ip>' 'bash -se' << $delimiter\n   <command containing the unsanitized network>\n   $delimiter\n   ```\n   `escapedUserAtHost()` escapes `user@ip`, but the **command body** (the\n   network) is never escaped, so shell metacharacters in `network` are\n   interpreted by the remote `bash`. `;` breaks out of the `docker network\n   inspect` token, the attacker command runs, and `#` comments out the rest of\n   the line. The same unsanitized interpolation exists in\n   `Destination/Show.php::delete()` and across `ApplicationDeploymentJob`,\n   `DatabaseBackupJob`, `StartService`, `Init`, and `proxy.php`.\n\nFix (PR #9228, released in `4.0.0-beta.471`, commit\n`3d1b9f53a0aec74468be75675bcaaaed0fd41d46`; release tag commit\n`914d7e0b50505bc1fd56c34974fca09ad354e92a`):\n\n- Adds `DOCKER_NETWORK_PATTERN = '/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/'` in\n  `app/Support/ValidationPatterns.php` and applies it as a `regex` validation\n  rule on the `network` field in both Livewire components.\n- Adds a `setNetworkAttribute()` mutator on `StandaloneDocker` and\n  `SwarmDocker` that throws `InvalidArgumentException` for names not matching\n  the pattern.\n- Applies `escapeshellarg()` to every shell usage of the network field.\n\n## Reproduction Steps\n\n1. Reference: `bundle/repro/reproduction_steps.sh` (self-contained).\n2. What the script does:\n   - Installs the `docker compose` v2 plugin if missing and pulls the official\n     Coolify images (`4.0.0-beta.470` vulnerable, `4.0.0-beta.471` fixed) plus\n     `postgres:15-alpine`, `redis:7-alpine`, `coolify-testing-host` (an\n     SSH-enabled target whose `authorized_keys` matches a hardcoded key) and\n     `coolify-realtime`.\n   - Brings up the real Coolify stack (Coolify Laravel app + Postgres + Redis +\n     Soketi + the SSH target) on a private Docker network, with\n     `IS_WINDOWS_DOCKER_DESKTOP=true` so the seeded localhost server targets\n     `coolify-testing-host`.\n   - Bootstraps an authenticated root user, the testing-host SSH private key,\n     and a server pointing at `coolify-testing-host` (root, port 22).\n   - From the `coolify-testing-host` container, runs an authenticated Python\n     client against the live Coolify web app (`http://coolify:8080`): logs in\n     via Laravel Fortify, loads `/server/{uuid}/destinations`, extracts the\n     `destination.new.docker` Livewire snapshot, and POSTs\n     `/livewire/update` setting `network = \"x;id > /tmp/coolify_net_pwned 2>&1 #\"`\n     and calling `submit()`.\n   - Repeats for the fixed image as a negative control.\n3. Expected evidence:\n   - Vulnerable build: the `StandaloneDocker::created` event runs the SSH\n     command with the injected network; `/tmp/coolify_net_pwned*` on the\n     SSH target contains `uid=0(root) gid=0(root) groups=0(root)`, and the\n     Livewire response redirects to the newly created destination.\n   - Fixed build: the request is rejected with\n     `The network field format is invalid.` (regex validation), no destination\n     is created, and no marker file appears.\n\n## Evidence\n\n- `bundle/logs/reproduction_steps.log` — full orchestrated run log.\n- `bundle/logs/stack_vulnerable.log`, `bundle/logs/setup_vulnerable.log` —\n  vulnerable stack bring-up and bootstrap.\n- `bundle/logs/vulnerable_attempt_1.log`, `bundle/logs/vulnerable_attempt_2.log`\n  — exploit output for the vulnerable build (Livewire `redirect` to\n  `/destination/<uuid>` → destination created).\n- `bundle/logs/vulnerable_marker_content.txt` — captured marker content\n  (`uid=0(root) gid=0(root) groups=0(root)`).\n- `bundle/logs/stack_fixed.log`, `bundle/logs/setup_fixed.log` — fixed stack.\n- `bundle/logs/fixed_attempt_1.log`, `bundle/logs/fixed_attempt_2.log` —\n  exploit output for the fixed build (`VALIDATION_BLOCKED` +\n  `The network field format is invalid.`, no redirect, no marker).\n- `bundle/repro/runtime_manifest.json` — runtime evidence manifest\n  (`entrypoint_kind=api_remote`, `service_started=true`,\n  `healthcheck_passed=true`, `target_path_reached=true`).\n\nKey reproduced excerpts (from the interactive verification run):\n- Vulnerable: `effects redirect=http://coolify:8080/destination/o1jvtyfefdzb8nxnyqv0lkd2`;\n  `standalone_dockers` row with\n  `network = x;id > /tmp/coolify_net_pwned 2>&1 #` on `server_id=1`; marker\n  `/tmp/coolify_net_pwned` = `uid=0(root) gid=0(root) groups=0(root)`.\n- Fixed: `effects redirect=None`, response snippet\n  `The network field format is invalid.`; no `standalone_dockers` row with the\n  malicious network; marker file absent.\n\nEnvironment: official Coolify Docker images on the shared Docker daemon; the\nauthenticated HTTP client runs in the `coolify-testing-host` container\n(reaches `coolify:8080` over the `coolifynet-repro` bridge); command execution\nis performed by Coolify over SSH (`bash -se` here-doc) from the `coolify`\ncontainer to `coolify-testing-host` as `root`.\n\n## Recommendations / Next Steps\n\n- Upgrade to Coolify `>= 4.0.0-beta.471` (PR #9228). The fix combines input\n  validation (regex whitelist matching Docker network naming rules), a model\n  mutator that rejects invalid names, and `escapeshellarg()` on every shell\n  usage of the network field.\n- Defense-in-depth: audit every `instant_remote_process()` /\n  `remote_process()` call site for user-controlled interpolation; prefer\n  `escapeshellarg()` / `escapeshellcmd()` and validated whitelists for any\n  field that becomes part of an SSH/Docker command string. Consider a\n  shared helper that forbids raw string interpolation of model attributes into\n  command strings.\n- Restrict destination-management permissions; do not expose the Coolify admin\n  interface publicly.\n\n## Additional Notes\n\n- Idempotency: the script tears the stack down (`docker compose down -v` +\n  hard cleanup of the fixed container/volume/network names) before each build,\n  and `setup.php` is idempotent (locates the root user by email, fixes the\n  password hash, ensures a single `coolify-testing-host` server). Two\n  attempts are run per build with distinct marker paths (hence distinct\n  network strings) to avoid the `(server_id, network)` unique constraint.\n- The Docker daemon in this environment is on a separate host from the script\n  execution sandbox, so published host ports are not reachable from the\n  sandbox; the authenticated client therefore runs inside the\n  `coolify-testing-host` container and reaches Coolify over the private Docker\n  network (`http://coolify:8080`). This is a genuine authenticated HTTP\n  request to the running Coolify service; the command execution crosses a real\n  SSH boundary from Coolify to the managed server.\n- No sanitizers (ASAN/UBSAN/etc.) are used; this is a real product-mode runtime\n  proof. The negative control (fixed image) demonstrates the patch closes the\n  injection without relying on sanitizer output.\n","cve_id":"CVE-2026-34594","cwe_id":"CWE-78 (OS Command Injection)","source_url":"coollabsio/coolify","reproduced_at":"2026-07-06T08:35:04.937626+00:00","duration_secs":3051.0,"tool_calls":313,"handoffs":2,"total_cost_usd":7.50639511,"agent_costs":{"judge":0.01019665,"repro":5.435690459999999,"support":0.10408338,"vuln_variant":1.9564246200000008},"cost_breakdown":{"judge":{"gpt-5.4-mini":0.01019665},"repro":{"accounts/fireworks/routers/glm-5p2-fast":5.435690459999999},"support":{"accounts/fireworks/routers/glm-5p2-fast":0.10408338},"vuln_variant":{"accounts/fireworks/routers/glm-5p2-fast":1.9564246200000008}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-07-06T08:35:35.973893+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":21189,"category":"reproduction_script"},{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":10834,"category":"analysis"},{"path":"bundle/vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":23835,"category":"reproduction_script"},{"path":"bundle/vuln_variant/rca_report.md","filename":"rca_report.md","size":15714,"category":"analysis"},{"path":"bundle/artifact_promotion_manifest.json","filename":"artifact_promotion_manifest.json","size":13977,"category":"other"},{"path":"bundle/vuln_variant/source_identity.json","filename":"source_identity.json","size":1743,"category":"other"},{"path":"bundle/vuln_variant/root_cause_equivalence.json","filename":"root_cause_equivalence.json","size":2333,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":1061,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":1246,"category":"other"},{"path":"bundle/logs/vulnerable_marker_1.txt","filename":"vulnerable_marker_1.txt","size":76,"category":"other"},{"path":"bundle/logs/vulnerable_attempt_1.log","filename":"vulnerable_attempt_1.log","size":346,"category":"log"},{"path":"bundle/logs/fixed_attempt_1.log","filename":"fixed_attempt_1.log","size":402,"category":"log"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":6856,"category":"log"},{"path":"bundle/logs/stack_vulnerable.log","filename":"stack_vulnerable.log","size":1047,"category":"log"},{"path":"bundle/logs/setup_vulnerable.log","filename":"setup_vulnerable.log","size":2322,"category":"log"},{"path":"bundle/logs/vulnerable_attempt_2.log","filename":"vulnerable_attempt_2.log","size":346,"category":"log"},{"path":"bundle/logs/vulnerable_marker_2.txt","filename":"vulnerable_marker_2.txt","size":76,"category":"other"},{"path":"bundle/logs/stack_fixed.log","filename":"stack_fixed.log","size":1047,"category":"log"},{"path":"bundle/logs/setup_fixed.log","filename":"setup_fixed.log","size":398,"category":"log"},{"path":"bundle/logs/fixed_attempt_2.log","filename":"fixed_attempt_2.log","size":402,"category":"log"},{"path":"bundle/vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":4558,"category":"other"},{"path":"bundle/vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":3411,"category":"other"},{"path":"bundle/vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":10530,"category":"documentation"},{"path":"bundle/vuln_variant/runtime_manifest.json","filename":"runtime_manifest.json","size":1674,"category":"other"},{"path":"bundle/logs/vuln_variant/debug_fixed_evidence.txt","filename":"debug_fixed_evidence.txt","size":685,"category":"other"},{"path":"bundle/logs/vuln_variant/vulnerable_marker_1.txt","filename":"vulnerable_marker_1.txt","size":80,"category":"other"},{"path":"bundle/logs/vuln_variant/reproduction_steps.log","filename":"reproduction_steps.log","size":4326,"category":"log"},{"path":"bundle/logs/vuln_variant/stack_vulnerable.log","filename":"stack_vulnerable.log","size":1059,"category":"log"},{"path":"bundle/logs/vuln_variant/setup_vulnerable.log","filename":"setup_vulnerable.log","size":597,"category":"log"},{"path":"bundle/logs/vuln_variant/vulnerable_attempt_1.log","filename":"vulnerable_attempt_1.log","size":719,"category":"log"},{"path":"bundle/logs/vuln_variant/vulnerable_attempt_2.log","filename":"vulnerable_attempt_2.log","size":719,"category":"log"},{"path":"bundle/logs/vuln_variant/vulnerable_marker_2.txt","filename":"vulnerable_marker_2.txt","size":80,"category":"other"},{"path":"bundle/logs/vuln_variant/stack_fixed.log","filename":"stack_fixed.log","size":1059,"category":"log"},{"path":"bundle/logs/vuln_variant/setup_fixed.log","filename":"setup_fixed.log","size":398,"category":"log"},{"path":"bundle/logs/vuln_variant/fixed_attempt_1.log","filename":"fixed_attempt_1.log","size":464,"category":"log"},{"path":"bundle/logs/vuln_variant/fixed_attempt_2.log","filename":"fixed_attempt_2.log","size":464,"category":"log"}]}