# REPRO-2026-00275: Coolify terminal WebSocket endpoints lack proper authorization checks, allowing low-privileged members to access terminal functionality and achieve remote command execution on managed hosts. ## Summary Status: published Severity: critical Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00275 CVE: CVE-2026-34047 ## Package Name: coollabsio/coolify Ecosystem: Composer Affected: <= 4.0.0-beta.470 Fixed: 4.0.0-beta.471 ## Root Cause ## 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 `/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. ## Reproduction Details Reproduced: 2026-07-08T04:53:25.898Z Duration: 2642 seconds Tool calls: 421 Turns: Unknown Handoffs: 3 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00275 pruva-verify CVE-2026-34047 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00275&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00275/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-34047 - Source: https://nvd.nist.gov/vuln/detail/CVE-2026-34047 ## Artifacts - bundle/repro/reproduction_steps.sh (reproduction_script, 29864 bytes) - bundle/repro/rca_report.md (analysis, 10476 bytes) - bundle/vuln_variant/reproduction_steps.sh (reproduction_script, 13801 bytes) - bundle/vuln_variant/rca_report.md (analysis, 10721 bytes) - bundle/artifact_promotion_manifest.json (other, 10203 bytes) - bundle/artifact_promotion_report.json (other, 14307 bytes) - bundle/vuln_variant/source_identity.json (other, 1091 bytes) - bundle/vuln_variant/root_cause_equivalence.json (other, 1449 bytes) - bundle/logs/vuln_result.txt (other, 161 bytes) - bundle/logs/fixed_result.txt (other, 43694 bytes) - bundle/repro/validation_verdict.json (other, 799 bytes) - bundle/repro/runtime_manifest.json (other, 1213 bytes) - bundle/logs/reproduction_steps.log (log, 48322 bytes) - bundle/logs/ws_client_vuln.log (log, 245 bytes) - bundle/logs/managed_host_vuln.log (log, 1191 bytes) - bundle/logs/ws_client_fixed.log (log, 45 bytes) - bundle/logs/terminal_vuln.log (log, 2196 bytes) - bundle/logs/terminal_fixed.log (log, 1620 bytes) - bundle/logs/managed_host_fixed.log (log, 106 bytes) - bundle/logs/server.log (log, 603 bytes) - bundle/vuln_variant/patch_analysis.md (documentation, 6200 bytes) - bundle/vuln_variant/variant_manifest.json (other, 4427 bytes) - bundle/vuln_variant/validation_verdict.json (other, 2544 bytes) - bundle/vuln_variant/runtime_manifest.json (other, 1113 bytes) - bundle/logs/vuln_variant/reproduction_steps.log (log, 3461 bytes) - bundle/logs/vuln_variant/fixed_api_token_private_key_probe.log (log, 560 bytes) - bundle/logs/vuln_variant/vulnerable_api_token_private_key_probe.log (log, 560 bytes) - bundle/logs/vuln_variant/vulnerable_version.txt (other, 41 bytes) - bundle/logs/vuln_variant/fixed_version.txt (other, 41 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00275 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00275/artifacts/bundle/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00275 ## 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