# 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 <t> ssh ... 'root'@'<ip>' 'bash -se' << $delimiter
   <command containing the unsanitized network>
   $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/<uuid>` → 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.
