Skip to content

fix(security): missing authorization on docker/terminal WebSocket handlers (member -> root)#4865

Merged
Siumauricio merged 9 commits into
canaryfrom
fix/authz-websocket-handlers
Jul 20, 2026
Merged

fix(security): missing authorization on docker/terminal WebSocket handlers (member -> root)#4865
Siumauricio merged 9 commits into
canaryfrom
fix/authz-websocket-handlers

Conversation

@Siumauricio

@Siumauricio Siumauricio commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

The WebSocket handlers under apps/dokploy/server/wss/ authenticated the session but did not authorize it beyond an organization check:

  • Any authenticated member could open a root shell in any container (/docker-container-terminal), read any container's logs (/docker-container-logs) or stats (/docker-stats).
  • The /terminal local branch (serverId=local) opened a root SSH shell on the control-plane host to any member.
  • No service-level check: a member restricted to specific services could reach other services' containers.

Fix

New authorization layer server/wss/authorize.ts, applied after the session check:

  • container ops (terminal/logs/stats): require the docker permission (owner/admin, or a member granted 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), require checkServiceAccess.
  • host/server terminal (/terminal): local host root terminal → owner/admin only; remote server terminal → gated on server access.

The frontend passes serviceId when 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-terminal with real session cookies on localhost:3000:

  • member (docker.read = false) → CLOSED code=4003 "Not authorized".
  • owner → allowed.

Plus unit tests (__test__/wss/authorize.test.ts) and the full wss suite pass; tsc --noEmit clean (cold).

Known limitation (follow-up)

serviceId is supplied by the client and is not cross-checked against containerId, so it hardens the normal UI flow and adds a service-access layer, but a crafted request could pair an owned serviceId with another container's id. Fully binding containerId → service requires resolving the container's labels server-side (docker inspect → appName → service) — tracked as a follow-up hardening. The serviceId gate 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

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
@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Jul 20, 2026
…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
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:M This PR changes 30-99 lines, ignoring generated files. labels Jul 20, 2026
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. and removed size:L This PR changes 100-499 lines, ignoring generated files. labels Jul 20, 2026
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:M This PR changes 30-99 lines, ignoring generated files. labels Jul 20, 2026
Siumauricio and others added 5 commits July 20, 2026 00:48
…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.
@Siumauricio
Siumauricio merged commit 393fb92 into canary Jul 20, 2026
4 checks passed
@Siumauricio
Siumauricio deleted the fix/authz-websocket-handlers branch July 20, 2026 21:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant