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.
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
Coolify is a self-hosted PaaS and deployment platform.
It manages:
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.
Terminal features are some of the highest-value surfaces in infrastructure software.
Why?
Because any mismatch between:
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.
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:
That made the bootstrap routes the right place to look.
And that is where the issue was.
The root cause was an authorization mismatch between the terminal UI and the terminal websocket bootstrap routes.
At the vulnerable revision:
GET /terminal was protected by can.access.terminalPOST /terminal/auth only checked auth()->check()POST /terminal/auth/ips only checked auth()->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/terminal/auth/ipsThat is the whole bug chain.
Because a normal team member could build the needed inputs from the regular application surface:
/servers exposed visible server UUIDs/server/{uuid} exposed ip, user, and port in rendered form fields/security/private-key exposed visible team private key UUIDs/var/www/html/storage/app/ssh/keys/ssh_key@<uuid>So the exploit path was straightforward:
/terminal/auth/terminal/auth/ips/terminal/wsThat is not a theoretical mismatch. That is a practical backend authorization failure.
The important distinction is backend trust and command execution.
Plenty of bugs look like:
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:
It was:
That is why this was a real security issue.
I validated this in a controlled local lab built from:
The lab used:
http://127.0.0.1:18000test2@example.comlocalhost -> coolify-testing-host:22 as rootsshws://127.0.0.1:6002/terminal/wsThe member account was not intended to have terminal access through the normal admin-facing UI.
That established the expected security boundary.
Using the member session, I sent:
POST /terminal/authPOST /terminal/auth/ipsBoth succeeded.
/terminal/auth/ips returned terminal-authorized hosts including:
That proved the backend bootstrap routes trusted the member session.
From normal authenticated pages, the same member could enumerate:
That was enough to drive the terminal path without needing secret key material disclosure.
Using the same authenticated session and XSRF token, I connected to:
The payload used the same command format expected by the terminal backend:
The websocket returned:
That was the important proof.
Not just:
But actual command execution on the managed host through the admin-only terminal path.
One part of this chain would already have been interesting.
For example:
/terminal/auth/terminal/auth/ipsBut that would still leave room for dismissal.
The stronger validation was end-to-end:
That closes the gap between "authorization bug in theory" and "practical infrastructure impact in reality."
It also made severity much easier to defend.
This issue was properly classified as Critical.
The classification was:
That makes sense.
The claim is not that an unauthenticated attacker can get shell access from nothing.
The claim is that:
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.
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:
That is far beyond an ordinary member-permission bug.
The minimum correct fix is straightforward:
can.access.terminal to both POST /terminal/auth and POST /terminal/auth/ipsThat 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:
That is the kind of remediation you want for a terminal feature:
This issue was reported privately through GitHub's security reporting flow.
The report included:
The issue was later assigned:
CVE-2026-34048
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.
None of that matters if:
Once a platform manages infrastructure, authorization mismatches stop being ordinary access-control mistakes. They become infrastructure-impacting vulnerabilities.
That is the real takeaway.
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.