CONTENTS
Intro
I found this issue while reviewing Coolify, an open-source self-hosted PaaS, with a very direct security question in mind:
Is terminal access actually enforced at the backend trust boundary, or only in the UI?
In this case, the answer was bad.
Coolify intended terminal access to be restricted to team administrators and owners, but the realtime terminal bootstrap routes only checked whether the user was logged in. That let a low-privileged team member satisfy the websocket terminal trust checks and reach command execution on team servers.
I validated this end-to-end in a local lab built from the vulnerable revision and later reported it privately. The issue was assigned CVE-2026-34048 with:
Coolify: Coolify on GitHub
CVE: CVE-2026-34048
This affected Coolify, an open-source self-hosted PaaS. On its official site, Coolify states that it has 3,641+ cloud customers, and presents itself as a platform for deploying websites, databases, web applications, and 280+ one-click services. Its official v4.0 changelog also states that thousands of companies and people have been using Coolify in production for 1-2 years.
Attack Chain
low-privileged team member session -> /terminal/auth and /terminal/auth/ips only check logged-in state -> realtime websocket trusts those responses -> member enumerates team server and visible SSH key UUID -> /terminal/ws accepts the session -> SSH-backed PTY is spawned -> shell access on team server
What Coolify Does
Coolify is a self-hosted PaaS and deployment platform.
It manages:
- servers
- applications
- deployments
- private keys
- team permissions
- terminal access to managed infrastructure
That last capability is the important one here.
Once a platform can open terminals to managed hosts, its authorization model is not just application logic anymore. It becomes an infrastructure trust boundary.
The important question was not whether the /terminal page looked admin-only.
The real question was:
Does the backend terminal path actually enforce that same authorization boundary when the websocket session is created?
In this case, it did not.
Why This Bug Was Worth Looking At
Terminal features are some of the highest-value surfaces in infrastructure software.
Why?
Because any mismatch between:
- UI authorization
- backend authorization
- websocket bootstrap logic
- host command execution
can turn a normal application user into a shell-capable operator.
That is exactly why this surface was worth testing.
I was not looking for random crashes or cosmetic permission bugs.
I was looking for a stronger class of failure:
Does an admin-only feature rely on a weaker backend trust check than the UI suggests?
That was the right question.
The Boundary I Focused On
I did not approach Coolify by fuzzing random endpoints and hoping for something interesting.
The stronger path was to identify the highest-risk boundary first.
For Coolify, that boundary was the terminal workflow:
- the UI says terminal access is restricted
- the terminal service is websocket-based
- websocket services usually have separate bootstrap trust logic
- terminal commands ultimately cross from application state into host execution
That made the bootstrap routes the right place to look.
And that is where the issue was.
Root Cause
The root cause was an authorization mismatch between the terminal UI and the terminal websocket bootstrap routes.
At the vulnerable revision:
GET /terminalwas protected bycan.access.terminalPOST /terminal/authonly checkedauth()->check()POST /terminal/auth/ipsonly checkedauth()->check()
That means the UI was gated by terminal authorization, but the backend trust boundary was gated by simple authenticated-session presence.
The realtime service then trusted those two routes completely.
In docker/coolify-realtime/terminal-server.js:
verifyClient()POSTed to/terminal/auth- websocket session setup POSTed to
/terminal/auth/ips - the websocket handler accepted attacker-supplied terminal command input after checking only whether the target host appeared in the returned host list
That is the whole bug chain.
Why this is exploitable
Because a normal team member could build the needed inputs from the regular application surface:
/serversexposed visible server UUIDs/server/{uuid}exposedip,user, andportin rendered form fields/security/private-keyexposed visible team private key UUIDs- the terminal path referenced keys through deterministic paths of the form:
So the exploit path was straightforward:
- log in as a non-admin team member
- call
/terminal/auth - call
/terminal/auth/ips - enumerate a visible server
- enumerate a visible key UUID
- connect to
/terminal/ws - send the same SSH command shape the backend expects
- receive shell output from a team host
That is not a theoretical mismatch. That is a practical backend authorization failure.
What Makes This a Security Issue, Not Just a UI Mismatch
The important distinction is backend trust and command execution.
Plenty of bugs look like:
- "the button is hidden"
- "the page is blocked"
- "the UI says you should not be here"
That alone is not enough.
The real question is:
Can the lower-privileged user still satisfy the backend trust checks that matter?
Here, the answer was yes.
This was not:
- a broken menu
- a missing frontend check
- a cosmetic routing problem
It was:
- websocket bootstrap authorization too weak
- terminal host authorization derived from that weak trust boundary
- actual shell access on managed infrastructure
That is why this was a real security issue.
PoC
I validated this in a controlled local lab built from:
The lab used:
- base URL:
http://127.0.0.1:18000 - low-privileged member account:
test2@example.com - target server:
localhost -> coolify-testing-host:22 as root - visible key UUID:
ssh - websocket endpoint:
ws://127.0.0.1:6002/terminal/ws
Step 1: confirm the UI boundary
The member account was not intended to have terminal access through the normal admin-facing UI.
That established the expected security boundary.
Step 2: call the bootstrap routes directly
Using the member session, I sent:
POST /terminal/authPOST /terminal/auth/ips
Both succeeded.
/terminal/auth/ips returned terminal-authorized hosts including:
That proved the backend bootstrap routes trusted the member session.
Step 3: enumerate server and key metadata
From normal authenticated pages, the same member could enumerate:
- visible server UUIDs
- server connection fields
- visible team private key UUIDs
That was enough to drive the terminal path without needing secret key material disclosure.
Step 4: open the terminal websocket
Using the same authenticated session and XSRF token, I connected to:
Step 5: send the terminal command payload
The payload used the same command format expected by the terminal backend:
Step 6: observe remote shell output
The websocket returned:
That was the important proof.
Not just:
- route access
- not just websocket acceptance
- not just metadata exposure
But actual command execution on the managed host through the admin-only terminal path.
Why This PoC Was Strong
One part of this chain would already have been interesting.
For example:
- member access to
/terminal/auth - or member access to
/terminal/auth/ips
But that would still leave room for dismissal.
The stronger validation was end-to-end:
- member session
- backend bootstrap success
- websocket acceptance
- PTY creation
- remote shell output
That closes the gap between "authorization bug in theory" and "practical infrastructure impact in reality."
It also made severity much easier to defend.
Severity and Classification
This issue was properly classified as Critical.
The classification was:
- CWE-862: Missing Authorization
- CVSS:
That makes sense.
The claim is not that an unauthenticated attacker can get shell access from nothing.
The claim is that:
- a low-privileged team member
- can satisfy backend terminal trust checks
- and reach command execution on team infrastructure
That is a major scope change from application RBAC failure into managed-host impact.
So even though privileges are low rather than none, the result is still clearly critical.
Why This Was Still Worth Reporting
Some people underrate vulnerabilities that start with PR:L.
That is a mistake when the affected feature is terminal access.
The real question is not:
"Was the attacker already logged in?"
The real question is:
"What can that lower-privileged user reach once backend authorization is wrong?"
In this case, the answer was:
- host selection data
- terminal bootstrap trust
- SSH-backed PTY execution
- shell access on team servers
That is far beyond an ordinary member-permission bug.
Fix Analysis
The minimum correct fix is straightforward:
- apply
can.access.terminalto bothPOST /terminal/authandPOST /terminal/auth/ips - ensure non-admin members are denied from both routes
- add regression coverage for:
- unauthenticated users denied
- authenticated members denied
- authorized admins and owners allowed
That addresses the immediate trust-boundary failure.
In my local validation patch, applying the terminal authorization middleware to those two routes removed the member-to-terminal path.
But the stronger lesson is that the backend should not trust attacker-controlled SSH command strings as the primary source of target metadata.
Recommended hardening is:
- bind terminal requests to a server or container identifier authorized server-side
- re-validate authorization when the command is executed, not only when the websocket is opened
- reduce reliance on client-supplied terminal command structure for security decisions
That is the kind of remediation you want for a terminal feature:
- fix the immediate missing authorization
- then tighten the deeper trust model
Disclosure
This issue was reported privately through GitHub's security reporting flow.
The report included:
- the authorization mismatch
- the affected routes
- the realtime backend trust path
- a working local lab validation
- end-to-end proof showing remote shell output
The issue was later assigned:
CVE-2026-34048
What This Bug Actually Teaches
The main lesson here is simple:
admin-only UI does not matter if the backend bootstrap channel trusts weaker state.
That is the real problem class.
- A page can be protected correctly.
- A menu can be hidden correctly.
- A terminal screen can be blocked correctly.
None of that matters if:
- the websocket bootstrap path checks only for login state
- the terminal backend trusts those bootstrap responses
- and the resulting session can reach host command execution
Once a platform manages infrastructure, authorization mismatches stop being ordinary access-control mistakes. They become infrastructure-impacting vulnerabilities.
That is the real takeaway.
Key Points
- websocket bootstrap endpoints are real security boundaries
- UI-only authorization is not enough for terminal features
- low-privileged users can still produce critical impact when backend trust is wrong
- enumeration of server metadata plus visible key UUIDs made this bug practical
- end-to-end runtime validation matters when defending severity
- the right fix is consistent backend authorization, not stronger frontend gating
Final Words
This vulnerability was not about a clever payload.
It was about identifying the right trust boundary.
Coolify meant terminal access to be admin-only. But the realtime terminal backend trusted routes that only checked whether the user was logged in.
From there, a low-privileged team member could drive the websocket terminal path and reach shell execution on a team server.
That is why this became CVE-2026-34048.
