# REPRO-2026-00245: Coolify authenticated command injection in Destination Network Management ## Summary Status: published Severity: high Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00245 CVE: CVE-2026-34594 ## Package Name: Unknown Ecosystem: Unknown Affected: Unknown Fixed: Unknown ## Root Cause # CVE-2026-34594 — Root Cause Analysis ## Summary Coolify (self-hosted PaaS, Laravel/Livewire application) before `4.0.0-beta.471` contains an authenticated command-injection vulnerability in the **Destination Network Management** functionality. The `network` field of a Docker Destination (`App\Livewire\Destination\New\Docker` and `App\Livewire\Destination\Show`) was only validated as `['required','string']`, so an authenticated user with destination-management permissions could submit a value containing shell metacharacters. That value is stored on the `StandaloneDocker` / `SwarmDocker` model and later interpolated **unsanitized** into shell commands that Coolify executes on the destination's server over SSH (`instant_remote_process()` → `SshMultiplexingHelper::generateSshCommand()` → `ssh ... 'bash -se' << $delimiter`). Because the network string becomes the body of a here-doc fed to `bash -se` on the remote host, an attacker-controlled `network` such as `x;id > /tmp/pwned #` results in arbitrary command execution **as root** on the Coolify-managed server at the moment the destination is created (the `StandaloneDocker::created` boot event) and again when it is deleted. ## Impact - **Package/component affected:** `coollabsio/coolify` — Destination Network Management (`app/Livewire/Destination/New/Docker.php`, `app/Livewire/Destination/Show.php`, `app/Models/StandaloneDocker.php`, `app/Models/SwarmDocker.php`, and every shell consumer of `->destination->network`: `ApplicationDeploymentJob`, `DatabaseBackupJob`, `StartService`, `Init` command, `bootstrap/helpers/proxy.php`). - **Affected versions:** Coolify `< 4.0.0-beta.471` (verified on the official image `ghcr.io/coollabsio/coolify:4.0.0-beta.470`). - **Risk level:** High. Coolify executes SSH commands with the destination server's user (default `root`). The injected command runs with that privilege on the managed host, enabling full server compromise, access to the Docker socket, lateral movement, and exposure of all managed resources / secrets. Exploitation requires only an authenticated user who can create a destination (the `StandaloneDockerPolicy::create` check returns `true` for any authenticated user; `createAnyResource` likewise returns `true`). ## Impact Parity - **Disclosed/claimed maximum impact:** Authenticated command execution ("arbitrary command execution on the Coolify host") via the Destination Network Management network field. - **Reproduced impact from this run:** Authenticated command execution as `root` on the destination server. The injected `id` command wrote `uid=0(root) gid=0(root) groups=0(root)` to a marker file on the managed server, proving arbitrary command execution with root privileges through the authenticated web request. - **Parity:** `full` — the claimed authenticated command injection was demonstrated end-to-end against the real product, and the fixed build rejects the same input with no execution. ## Root Cause The `network` attribute of a Docker Destination is attacker-controlled input that flows into privileged shell commands without validation or escaping. Vulnerable code path (beta.470): 1. `app/Livewire/Destination/New/Docker.php` declares the field with only `#[Validate(['required','string'])]` (no character whitelist), and `submit()` calls `StandaloneDocker::create(['network' => $this->network, ...])`. 2. `app/Models/StandaloneDocker.php` `boot()` registers a `static::created` hook that runs: ```php instant_remote_process([ "docker network inspect $newStandaloneDocker->network >/dev/null 2>&1 || docker network create --driver overlay --attachable $newStandaloneDocker->network >/dev/null", ], $server, false); ``` The network is interpolated directly into the command string. 3. `instant_remote_process()` builds an SSH command via `SshMultiplexingHelper::generateSshCommand()`, which sends the command as the body of a here-doc to `bash -se` on the remote server: ``` timeout ssh ... 'root'@'' 'bash -se' << $delimiter $delimiter ``` `escapedUserAtHost()` escapes `user@ip`, but the **command body** (the network) is never escaped, so shell metacharacters in `network` are interpreted by the remote `bash`. `;` breaks out of the `docker network inspect` token, the attacker command runs, and `#` comments out the rest of the line. The same unsanitized interpolation exists in `Destination/Show.php::delete()` and across `ApplicationDeploymentJob`, `DatabaseBackupJob`, `StartService`, `Init`, and `proxy.php`. Fix (PR #9228, released in `4.0.0-beta.471`, commit `3d1b9f53a0aec74468be75675bcaaaed0fd41d46`; release tag commit `914d7e0b50505bc1fd56c34974fca09ad354e92a`): - Adds `DOCKER_NETWORK_PATTERN = '/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/'` in `app/Support/ValidationPatterns.php` and applies it as a `regex` validation rule on the `network` field in both Livewire components. - Adds a `setNetworkAttribute()` mutator on `StandaloneDocker` and `SwarmDocker` that throws `InvalidArgumentException` for names not matching the pattern. - Applies `escapeshellarg()` to every shell usage of the network field. ## Reproduction Steps 1. Reference: `bundle/repro/reproduction_steps.sh` (self-contained). 2. What the script does: - Installs the `docker compose` v2 plugin if missing and pulls the official Coolify images (`4.0.0-beta.470` vulnerable, `4.0.0-beta.471` fixed) plus `postgres:15-alpine`, `redis:7-alpine`, `coolify-testing-host` (an SSH-enabled target whose `authorized_keys` matches a hardcoded key) and `coolify-realtime`. - Brings up the real Coolify stack (Coolify Laravel app + Postgres + Redis + Soketi + the SSH target) on a private Docker network, with `IS_WINDOWS_DOCKER_DESKTOP=true` so the seeded localhost server targets `coolify-testing-host`. - Bootstraps an authenticated root user, the testing-host SSH private key, and a server pointing at `coolify-testing-host` (root, port 22). - From the `coolify-testing-host` container, runs an authenticated Python client against the live Coolify web app (`http://coolify:8080`): logs in via Laravel Fortify, loads `/server/{uuid}/destinations`, extracts the `destination.new.docker` Livewire snapshot, and POSTs `/livewire/update` setting `network = "x;id > /tmp/coolify_net_pwned 2>&1 #"` and calling `submit()`. - Repeats for the fixed image as a negative control. 3. Expected evidence: - Vulnerable build: the `StandaloneDocker::created` event runs the SSH command with the injected network; `/tmp/coolify_net_pwned*` on the SSH target contains `uid=0(root) gid=0(root) groups=0(root)`, and the Livewire response redirects to the newly created destination. - Fixed build: the request is rejected with `The network field format is invalid.` (regex validation), no destination is created, and no marker file appears. ## Evidence - `bundle/logs/reproduction_steps.log` — full orchestrated run log. - `bundle/logs/stack_vulnerable.log`, `bundle/logs/setup_vulnerable.log` — vulnerable stack bring-up and bootstrap. - `bundle/logs/vulnerable_attempt_1.log`, `bundle/logs/vulnerable_attempt_2.log` — exploit output for the vulnerable build (Livewire `redirect` to `/destination/` → destination created). - `bundle/logs/vulnerable_marker_content.txt` — captured marker content (`uid=0(root) gid=0(root) groups=0(root)`). - `bundle/logs/stack_fixed.log`, `bundle/logs/setup_fixed.log` — fixed stack. - `bundle/logs/fixed_attempt_1.log`, `bundle/logs/fixed_attempt_2.log` — exploit output for the fixed build (`VALIDATION_BLOCKED` + `The network field format is invalid.`, no redirect, no marker). - `bundle/repro/runtime_manifest.json` — runtime evidence manifest (`entrypoint_kind=api_remote`, `service_started=true`, `healthcheck_passed=true`, `target_path_reached=true`). Key reproduced excerpts (from the interactive verification run): - Vulnerable: `effects redirect=http://coolify:8080/destination/o1jvtyfefdzb8nxnyqv0lkd2`; `standalone_dockers` row with `network = x;id > /tmp/coolify_net_pwned 2>&1 #` on `server_id=1`; marker `/tmp/coolify_net_pwned` = `uid=0(root) gid=0(root) groups=0(root)`. - Fixed: `effects redirect=None`, response snippet `The network field format is invalid.`; no `standalone_dockers` row with the malicious network; marker file absent. Environment: official Coolify Docker images on the shared Docker daemon; the authenticated HTTP client runs in the `coolify-testing-host` container (reaches `coolify:8080` over the `coolifynet-repro` bridge); command execution is performed by Coolify over SSH (`bash -se` here-doc) from the `coolify` container to `coolify-testing-host` as `root`. ## Recommendations / Next Steps - Upgrade to Coolify `>= 4.0.0-beta.471` (PR #9228). The fix combines input validation (regex whitelist matching Docker network naming rules), a model mutator that rejects invalid names, and `escapeshellarg()` on every shell usage of the network field. - Defense-in-depth: audit every `instant_remote_process()` / `remote_process()` call site for user-controlled interpolation; prefer `escapeshellarg()` / `escapeshellcmd()` and validated whitelists for any field that becomes part of an SSH/Docker command string. Consider a shared helper that forbids raw string interpolation of model attributes into command strings. - Restrict destination-management permissions; do not expose the Coolify admin interface publicly. ## Additional Notes - Idempotency: the script tears the stack down (`docker compose down -v` + hard cleanup of the fixed container/volume/network names) before each build, and `setup.php` is idempotent (locates the root user by email, fixes the password hash, ensures a single `coolify-testing-host` server). Two attempts are run per build with distinct marker paths (hence distinct network strings) to avoid the `(server_id, network)` unique constraint. - The Docker daemon in this environment is on a separate host from the script execution sandbox, so published host ports are not reachable from the sandbox; the authenticated client therefore runs inside the `coolify-testing-host` container and reaches Coolify over the private Docker network (`http://coolify:8080`). This is a genuine authenticated HTTP request to the running Coolify service; the command execution crosses a real SSH boundary from Coolify to the managed server. - No sanitizers (ASAN/UBSAN/etc.) are used; this is a real product-mode runtime proof. The negative control (fixed image) demonstrates the patch closes the injection without relying on sanitizer output. ## Reproduction Details Reproduced: 2026-07-06T08:35:04.937Z Duration: 3051 seconds Tool calls: 313 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00245 pruva-verify CVE-2026-34594 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00245&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00245/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-34594 - Source: coollabsio/coolify ## Artifacts - bundle/repro/reproduction_steps.sh (reproduction_script, 21189 bytes) - bundle/repro/rca_report.md (analysis, 10834 bytes) - bundle/vuln_variant/reproduction_steps.sh (reproduction_script, 23835 bytes) - bundle/vuln_variant/rca_report.md (analysis, 15714 bytes) - bundle/artifact_promotion_manifest.json (other, 13977 bytes) - bundle/vuln_variant/source_identity.json (other, 1743 bytes) - bundle/vuln_variant/root_cause_equivalence.json (other, 2333 bytes) - bundle/repro/validation_verdict.json (other, 1061 bytes) - bundle/repro/runtime_manifest.json (other, 1246 bytes) - bundle/logs/vulnerable_marker_1.txt (other, 76 bytes) - bundle/logs/vulnerable_attempt_1.log (log, 346 bytes) - bundle/logs/fixed_attempt_1.log (log, 402 bytes) - bundle/logs/reproduction_steps.log (log, 6856 bytes) - bundle/logs/stack_vulnerable.log (log, 1047 bytes) - bundle/logs/setup_vulnerable.log (log, 2322 bytes) - bundle/logs/vulnerable_attempt_2.log (log, 346 bytes) - bundle/logs/vulnerable_marker_2.txt (other, 76 bytes) - bundle/logs/stack_fixed.log (log, 1047 bytes) - bundle/logs/setup_fixed.log (log, 398 bytes) - bundle/logs/fixed_attempt_2.log (log, 402 bytes) - bundle/vuln_variant/variant_manifest.json (other, 4558 bytes) - bundle/vuln_variant/validation_verdict.json (other, 3411 bytes) - bundle/vuln_variant/patch_analysis.md (documentation, 10530 bytes) - bundle/vuln_variant/runtime_manifest.json (other, 1674 bytes) - bundle/logs/vuln_variant/debug_fixed_evidence.txt (other, 685 bytes) - bundle/logs/vuln_variant/vulnerable_marker_1.txt (other, 80 bytes) - bundle/logs/vuln_variant/reproduction_steps.log (log, 4326 bytes) - bundle/logs/vuln_variant/stack_vulnerable.log (log, 1059 bytes) - bundle/logs/vuln_variant/setup_vulnerable.log (log, 597 bytes) - bundle/logs/vuln_variant/vulnerable_attempt_1.log (log, 719 bytes) - bundle/logs/vuln_variant/vulnerable_attempt_2.log (log, 719 bytes) - bundle/logs/vuln_variant/vulnerable_marker_2.txt (other, 80 bytes) - bundle/logs/vuln_variant/stack_fixed.log (log, 1059 bytes) - bundle/logs/vuln_variant/setup_fixed.log (log, 398 bytes) - bundle/logs/vuln_variant/fixed_attempt_1.log (log, 464 bytes) - bundle/logs/vuln_variant/fixed_attempt_2.log (log, 464 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00245 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00245/artifacts/bundle/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00245 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev