## Summary

CVE-2026-34047 is an authorization bypass in Coolify's terminal flow. The user-facing terminal pages are protected by the `can.access.terminal` / `canAccessTerminal` authorization gate, but the backend bootstrap endpoints used by the terminal WebSocket server (`POST /terminal/auth` and `POST /terminal/auth/ips`) only require authentication in the vulnerable release. A low-privileged authenticated team `member` can call those endpoints directly, obtain an authorized host list, connect to the real `/terminal/ws` WebSocket service, and cause Coolify's terminal server to spawn an SSH command toward a managed host.

## Impact

- **Package/component affected:** `coollabsio/coolify`, specifically the Laravel terminal authorization routes in `routes/web.php` and the `docker/coolify-realtime/terminal-server.js` WebSocket terminal backend.
- **Affected versions:** Coolify versions up to and including `4.0.0-beta.470` are affected according to the ticket.
- **Patched version:** `4.0.0-beta.471` / the middleware fix adding `can.access.terminal` to the terminal bootstrap routes.
- **Risk level and consequences:** Critical. An authenticated low-privileged team member can bypass terminal authorization and reach the command-execution path intended only for team administrators/owners. In a real Coolify deployment, this can provide command execution on configured/managed hosts through Coolify's terminal SSH flow.

## Impact Parity

- **Disclosed/claimed maximum impact:** Code execution / remote command execution on managed hosts by an authenticated low-privileged `Member` user.
- **Reproduced impact from this run:** Full WebSocket terminal command execution path was reproduced. The script authenticates as a Coolify team `member`, receives HTTP 200 from `/terminal/auth` and `/terminal/auth/ips`, opens `/terminal/ws`, sends a terminal SSH command payload, and receives output from a managed-host SSH peer after the host executes the attacker-supplied shell commands (`printf 'REPRO_WS_COMMAND_EXECUTED\n'; whoami; hostname; pwd`).
- **Parity:** `full` for the requested WebSocket terminal flow and code/command-execution impact. The managed host is a local unprivileged SSH peer created by the script, but the boundary crossed is the real Coolify terminal WebSocket server and OpenSSH command path.
- **Not demonstrated:** The script does not retrieve a production SSH private key or spawn a root shell on a real external host. It proves the terminal authorization bypass reaches real command execution through the product WebSocket/SSH path using a controlled local managed-host peer.

## Root Cause

In the vulnerable route definitions, Coolify protects the visible terminal pages with `can.access.terminal` but leaves the terminal backend authorization endpoints without that middleware:

```php
Route::get('/terminal', TerminalIndex::class)->name('terminal')->middleware('can.access.terminal');
Route::post('/terminal/auth', function () {
    if (auth()->check()) {
        return response()->json(['authenticated' => true], 200);
    }
    return response()->json(['authenticated' => false], 401);
})->name('terminal.auth');

Route::post('/terminal/auth/ips', function () {
    if (auth()->check()) {
        $team = auth()->user()->currentTeam();
        $ipAddresses = $team->servers
            ->where('settings.is_terminal_enabled', true)
            ->pluck('ip')
            ->filter()
            ->values();
        return response()->json(['ipAddresses' => $ipAddresses->all()], 200);
    }
    return response()->json(['ipAddresses' => []], 401);
})->name('terminal.auth.ips');
```

The real terminal WebSocket server (`docker/coolify-realtime/terminal-server.js`) trusts those endpoints. During WebSocket verification it calls `http://coolify:8080/terminal/auth`; on connection it calls `http://coolify:8080/terminal/auth/ips`; when a client sends a `command` message it parses the SSH target, checks only whether the target is present in the returned authorized IP list, and then calls `pty.spawn('ssh', ...)`. Because the backend route authorization is too weak, a low-privileged `member` receives the same authorization bootstrap data as an owner/admin.

The fix is to require the same terminal authorization gate on the backend bootstrap routes, e.g. adding `->middleware('can.access.terminal')` to both `terminal.auth` and `terminal.auth.ips`. The reproduction script applies this middleware as the fixed negative-control behavior and verifies that the `member` is blocked with HTTP 403 before WebSocket command execution.

## Reproduction Steps

1. Run `bundle/repro/reproduction_steps.sh` from the bundle root (or with `PRUVA_ROOT` pointing at the bundle).
2. The script:
   - Reuses the prepared Coolify repository at `<project_cache_dir>/repo` when available.
   - Checks out `v4.0.0-beta.470` and starts the real Laravel application with a SQLite test database.
   - Starts the real `docker/coolify-realtime/terminal-server.js` WebSocket service.
   - Creates an owner, a low-privileged `member`, a terminal-enabled server record for `127.0.0.1`, and a local managed-host SSH peer on `127.0.0.1:2222`.
   - Logs in as the member through Coolify's magic-link route and makes real HTTP requests to `POST /terminal/auth/ips` and `POST /terminal/auth`.
   - Connects to `ws://127.0.0.1:6002/terminal/ws` with the member session cookies and sends a terminal SSH command payload.
   - Verifies the vulnerable path returns command output from the managed host.
   - Applies the fixed middleware to the two terminal bootstrap routes, repeats the same member flow, and verifies the endpoints return 403 and no managed-host command executes.
3. Expected evidence of reproduction:
   - `bundle/logs/vuln_result.txt` shows `IPS_STATUS=200` and `AUTH_STATUS=200` for the member user.
   - `bundle/logs/terminal_vuln.log` shows WebSocket authentication, authorized IP retrieval, received command, parsed SSH metadata, and `Spawning PTY process for terminal session`.
   - `bundle/logs/managed_host_vuln.log` shows `MANAGED_HOST_PROCESS` with the attacker-supplied shell command and `MANAGED_HOST_PROCESS_EXIT ... output='REPRO_WS_COMMAND_EXECUTED...`.
   - `bundle/logs/ws_client_vuln.log` shows the WebSocket client received `REPRO_WS_COMMAND_EXECUTED`, `whoami`, `hostname`, and `pwd` output followed by `pty-exited`.
   - `bundle/logs/fixed_result.txt` shows `IPS_STATUS=403` and `AUTH_STATUS=403` for the same member role.

## Evidence

Key runtime artifacts from the final successful runs:

- `bundle/repro/reproduction_steps.sh` — self-contained reproducer.
- `bundle/repro/runtime_manifest.json` — runtime evidence manifest with `service_started=true`, `healthcheck_passed=true`, and `target_path_reached=true`.
- `bundle/logs/reproduction_steps.log` — orchestration log with final summary:
  - `Vulnerable: Member endpoints 200/200, WebSocket managed-host command execution=true`
  - `Fixed:      Member endpoints 403/403, WebSocket blocked=true`
  - `SUCCESS: CVE-2026-34047 reproduced with WebSocket command execution and fixed negative control`
- `bundle/logs/vuln_result.txt`:
  - `IPS_STATUS=200`
  - `IPS_BODY={"ipAddresses":["127.0.0.1","coolify-testing-host","host.docker.internal","localhost"]}`
  - `AUTH_STATUS=200`
  - `AUTH_BODY={"authenticated":true}`
- `bundle/logs/terminal_vuln.log` proves the real terminal backend path:
  - `Websocket client authentication succeeded.`
  - `Fetched authorized terminal hosts for websocket session.`
  - `Received websocket message. { ... keys: [ 'command' ] ... }`
  - `Parsed terminal command metadata. { targetHost: '127.0.0.1', ... }`
  - `Spawning PTY process for terminal session.`
  - `PTY process exited. { ... exitCode: 0 ... }`
- `bundle/logs/managed_host_vuln.log` proves command execution on the managed-host peer:
  - `MANAGED_HOST_PROCESS label=vuln command=" printf 'REPRO_WS_COMMAND_EXECUTED\\n'; whoami; hostname; pwd "`
  - `MANAGED_HOST_PROCESS_EXIT label=vuln rc=0 output='REPRO_WS_COMMAND_EXECUTED\nvscode\n...\n/tmp\n'`
- `bundle/logs/ws_client_vuln.log` proves the WebSocket client received command output:
  - `CLIENT_MSG="pty-ready"`
  - `CLIENT_MSG="REPRO_WS_COMMAND_EXECUTED\r\nvscode\r\n...\r\n/tmp\r\n"`
  - `CLIENT_MSG="pty-exited"`
- `bundle/logs/fixed_result.txt` proves the negative control:
  - `IPS_STATUS=403`
  - `AUTH_STATUS=403`
  - Response message: `Access to terminal functionality is restricted to team administrators`.
- `bundle/logs/ws_client_fixed.log` shows the WebSocket handshake fails in the fixed path (`Unexpected server response: 500`) before any command reaches the managed host.
- `bundle/logs/managed_host_fixed.log` contains only server readiness and no `MANAGED_HOST_PROCESS`, confirming no command execution in the fixed negative control.

Environment details captured by the script include the vulnerable git commit (`575b0766d12bad2a78febff72ab59c017772bcf7` for `v4.0.0-beta.470`), local PHP version, Coolify Laravel service health, terminal WebSocket readiness, and managed-host SSH readiness.

## Recommendations / Next Steps

- Add `can.access.terminal` middleware to all terminal backend bootstrap routes, not only the visible terminal UI pages.
- Keep authorization checks server-side in the WebSocket/bootstrap flow; do not rely on frontend route visibility or UI navigation controls.
- Ensure `/terminal/auth` and `/terminal/auth/ips` enforce the same admin/owner authorization semantics as `/terminal` and resource terminal pages.
- Add regression tests for low-privileged team members covering:
  - `POST /terminal/auth` returns 403.
  - `POST /terminal/auth/ips` returns 403.
  - `/terminal/ws` fails closed and never reaches command execution when backend auth routes reject the session.
- Upgrade affected installations to `4.0.0-beta.471` or later.

## Additional Notes

- Idempotency: The final reproducer was run successfully multiple times. The last two full script executions completed with exit code 0 and reproduced the vulnerable-vs-fixed behavioral difference.
- The managed host is a controlled local SSH peer, which avoids requiring privileged system SSH setup while still proving that attacker input crosses Coolify's real HTTP/WebSocket boundary and reaches the real terminal-server `node-pty`/OpenSSH command execution path.
- The fixed negative control uses the middleware behavior from the patch to prove the same member session is blocked with 403 before WebSocket command execution.