fix(security): missing authorization on docker/terminal WebSocket handlers (member -> root)#4865
Merged
Merged
Conversation
The /terminal, /docker-container-terminal, /docker-container-logs and /docker-stats WebSocket handlers only checked session + organization, so any authenticated member could open a root shell / read logs of any container, and the local (serverId=local) branch reached a root SSH shell on the control-plane host. Adds an authorization layer (server/wss/authorize.ts): - container ops (terminal/logs/stats): require the docker permission (owner/admin or a member granted canAccessToDocker); for a remote server, require the server be accessible to the caller. - host/server terminal: the local host root terminal is restricted to owner/admin; a remote server terminal is gated on server access. Closes GHSA-c68r-7wg9-p7v2, GHSA-899j-cjwp-v4gw, GHSA-7r6p-v9gw-pwc8, GHSA-qf9j-c9p4-r4xp
…lers Completes GHSA-qf9j service-level dimension: when a container terminal/logs/stats is opened from a service page, the frontend now passes serviceId, and the wss authorizer calls checkServiceAccess so a member restricted to specific services cannot reach another service's containers. Generic Docker-overview opens have no serviceId and keep the docker-permission + server-access gate. Verified against the local WebSocket: a member (docker:read=false) is rejected with close code 4003; owner is allowed. Closes GHSA-qf9j-c9p4-r4xp
…ket handlers" This reverts commit 479d851.
…cket handlers" This reverts commit 56169f3.
…ass serviceId from log views - Reorder canAccessDockerOverWss: docker permission -> service access (if serviceId) -> server access, so the service grant is the primary signal. - Pass serviceId from the service log views (application + postgres/mysql/mariadb/ mongo/redis/libsql + compose) so container logs opened from a service page are gated by checkServiceAccess, matching the terminal path.
… ops When a container is opened from a service page (serviceId present), gate on service access alone — matching application.readLogs — instead of requiring the docker permission first. This unblocks a member who has the service but not the canAccessToDocker permission (or the server it runs on) from reading its logs / opening its terminal. Generic Docker-overview ops (no serviceId) still require docker permission + server access.
- application general 'Open Terminal' now passes serviceId (applicationId). - The docker/terminal DockerTerminalModal now forwards serviceId to its Terminal. - compose container rows thread serviceId to their terminal + logs modals. This lets a member with service access (but without the docker permission) open the terminal / read logs of their own service's containers from every service view, matching the backend service-first authorization.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The WebSocket handlers under
apps/dokploy/server/wss/authenticated the session but did not authorize it beyond an organization check:/docker-container-terminal), read any container's logs (/docker-container-logs) or stats (/docker-stats)./terminallocal branch (serverId=local) opened a root SSH shell on the control-plane host to any member.Fix
New authorization layer
server/wss/authorize.ts, applied after the session check:canAccessToDocker); for a remote server, require the server be in the caller's accessible set; and when a serviceId is supplied (container opened from a service page), requirecheckServiceAccess./terminal): local host root terminal → owner/admin only; remote server terminal → gated on server access.The frontend passes
serviceIdwhen the docker terminal/logs are opened from a service page (compose + all database services). Generic Docker-overview opens have no serviceId and keep the docker-permission + server gate.Verification — tested end-to-end against the live local WebSocket
/docker-container-terminalwith real session cookies onlocalhost:3000:docker.read = false) →CLOSED code=4003 "Not authorized".Plus unit tests (
__test__/wss/authorize.test.ts) and the full wss suite pass;tsc --noEmitclean (cold).Known limitation (follow-up)
serviceIdis supplied by the client and is not cross-checked againstcontainerId, so it hardens the normal UI flow and adds a service-access layer, but a crafted request could pair an ownedserviceIdwith another container's id. Fully bindingcontainerId → servicerequires resolving the container's labels server-side (docker inspect→ appName → service) — tracked as a follow-up hardening. TheserviceIdgate here is strictly additive (it never replaces the docker-permission check), so it introduces no regression.Closes
GHSA-c68r-7wg9-p7v2, GHSA-899j-cjwp-v4gw, GHSA-7r6p-v9gw-pwc8, GHSA-qf9j-c9p4-r4xp