Dokploy / Dokploy/dokploy

SSH terminal permission vulnerability

Open
#5,355 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

needs-triage🔍
Dominant language
TypeScript
Stars
37.4k
Forks
3k
Avg merge
1d 3h
Merged PRs (30d)
73

Description

To Reproduce

both files currently only check organization-level isolation (), with no per-member permission check at all. This means any authenticated member of the organization — even one with and in the UI — can still open a raw SSH shell to the host () or into any container (), as long as they can guess/obtain a valid /. This is exactly the gap I flagged earlier. server.organizationId !== session.activeOrganizationId canAccessToSSHKeys: false canAccessToDocker: false terminal.ts docker exec docker-container-terminal.ts serverId containerId

apps/dokploy/server/wss/terminal.ts — host SSH terminal

This is the more serious one: it opens a raw SSH shell to the server itself via 's terminal.ts:176-182. The only auth/authorization check is: terminal.ts:150-160 ssh2 Client

There's no check against , , or even the / permission from the access-control system. A regular role user with all boxes unchecked in your screenshot can still connect here directly if they know the (which is often visible in the dashboard URL for accessible projects). canAccessToSSHKeys accessedServers server:read ssh member serverId

apps/dokploy/server/wss/docker-container-terminal.ts — container exec terminal

Same pattern — it only checks org match before opening a over SSH docker-container-terminal.ts:61-68. No check for , and no check that the actually belongs to a service the member has access to (). A member could exec into any container on the shared server, not just their own, as long as they can guess/enumerate a docker exec -it sh canAccessToDocker containerId accessedServices containerId

Current vs. Expected behavior

What patching would need to do

For a proper fix, both handlers need to, after resolves / , look up the member's role/permissions (similar to + used elsewhere in permission.ts:141-177 ) and reject the connection if: validateRequest user session findMemberByUserId getLegacyOverrides

the member is not / and (for ) or (for ) is falsy, or owner admin canAccessToSSHKeys terminal.ts canAccessToDocker docker-container-terminal.ts

(ideally, for the container terminal) the given 's owning service is not in the member's — the current code has no mapping from to a / check at all, so this would require resolving which project/service owns that container before allowing exec, or containerId accessedServices containerId service accessedServices

for , the is not in the member's (this only matters for the Enterprise multi-server-assignment case, but even without a license, at minimum should gate it). terminal.ts serverId accessedServers canAccessToSSHKeys

This logic doesn't exist anywhere in these two files today, so it's not a config toggle you can flip — it needs actual code changes to the WebSocket handlers. connection

  1. apps/dokploy/server/wss/terminal.ts (host SSH terminal):
  • In the wssTerm.on("connection", ...) callback, after validateRequest gets user/session, in the non-isLocalServer branch, in addition to the existing server.organizationId !== session.activeOrganizationId check, you need to additionally query the member record of the user in the current organization (please refer to findMemberByUserId/findMemberById in packages/server/src/services/user.ts or permission resolution logic in packages/server/src/services/permission.ts).
  • If memberRecord.role is not owner or admin:
  • Must require memberRecord.canAccessToSSHKeys === true, otherwise call ws.close() and return (refer to the existing ws.close(); return; pattern in the file).
  • If the organization has a valid enterprise authorization (hasValidLicense, refer to the usage of getAccessibleServerIds in packages/server/src/services/server.ts), you should also verify whether serverId is in the memberRecord.accessedServers array, otherwise the connection will be refused.
  • The isLocalServer (serverId === "local") branch currently only allows non-IS_CLOUD environments. It is recommended that only the owner/admin role be allowed to access the local server terminal (because this is essentially a host-level operation), and non-owner/admin should be directly denied.
  1. apps/dokploy/server/wss/docker-container-terminal.ts (container exec terminal):
  • After validateRequest gets user/session, in the branch where serverId exists, in addition to the existing server.organizationId !== session.activeOrganizationId check, the member record is also queried.
  • If memberRecord.role is not owner/admin, memberRecord.canAccessToDocker === true must be required, otherwise the connection is refused (ws.close(4000, "Unauthorized") or similar).
  • Further (more strict isolation), the service/application/compose record to which it belongs should be checked based on containerId (refer to the association between application/compose and containerId/appName in the database schema, or the existing Docker service query function) to confirm whether the service is in the memberRecord.accessedServices array. If not, reject it to prevent members from exec into containers belonging to other tenants on the same shared server.
  • Local branches where serverId does not exist (else branches, spawn("docker", ...) is executed directly on the local host) are currently only allowed to be executed by any logged-in user under non-IS_CLOUD; it is recommended to also add owner/admin or canAccessToDocker permission verification, combined with containerId ownership verification.
  1. Supplement unit tests or integration tests to cover the following scenarios: members who are not owner/admin and do not have canAccessToDocker/canAccessToSSHKeys enabled should attempt to connect to the terminal (ws.close is called); members with corresponding permissions enabled and containerId/serverId in their accessible list should be able to connect normally.

Please read before modifying packages/server/src/services/permission.ts, packages/server/src/services/user.ts (findMemberByUserId, findMemberB yId), packages/server/src/services/server.ts (getAccessibleServerIds, hasValidLicense) and other files, reuse existing permission query/judgment functions to avoid repeated implementation logic. Also confirm whether the session returned by validateRequest already contains activeOrganizationId, and whether there is a reusable auxiliary function that constructs PermissionCtx based on the WebSocket request context.

Provide environment information
Ubuntu 24.04.4 LTS
v0.30.5
Which area(s) are affected? (Select all that apply)

Docker, Local Development, Cloud Version

Are you deploying the applications where Dokploy is installed or on a remote server?

Same server where Dokploy is installed

Additional context

No response

Will you send a PR to fix it?

No

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with apps/dokploy/server/wss/terminal.ts and apps/dokploy/server/wss/docker-container-terminal.ts, then read permission.ts, user.ts, and server.ts for existing member, server, and license checks. Trace validateRequest and the WebSocket connection branches before identifying how container ownership maps to services. Done means unauthorized terminal connections are rejected, permitted scoped connections still work, and tests cover both cases.

Written by the indexing model from the issue text.

Assessment

Tech stack
docker, typescript
Domain
backend, devops, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.