SSH terminal permission vulnerability
Nobody has claimed this yet.
- 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
apps/dokploy/server/wss/terminal.ts(host SSH terminal):
- In the
wssTerm.on("connection", ...)callback, aftervalidateRequestgetsuser/session, in the non-isLocalServerbranch, in addition to the existingserver.organizationId !== session.activeOrganizationIdcheck, you need to additionally query thememberrecord of the user in the current organization (please refer tofindMemberByUserId/findMemberByIdinpackages/server/src/services/user.tsor permission resolution logic inpackages/server/src/services/permission.ts). - If
memberRecord.roleis notowneroradmin: - Must require
memberRecord.canAccessToSSHKeys === true, otherwise callws.close()and return (refer to the existingws.close(); return;pattern in the file). - If the organization has a valid enterprise authorization (
hasValidLicense, refer to the usage ofgetAccessibleServerIdsinpackages/server/src/services/server.ts), you should also verify whetherserverIdis in thememberRecord.accessedServersarray, otherwise the connection will be refused. - The
isLocalServer(serverId === "local") branch currently only allows non-IS_CLOUD environments. It is recommended that only theowner/adminrole be allowed to access the local server terminal (because this is essentially a host-level operation), and non-owner/admin should be directly denied.
apps/dokploy/server/wss/docker-container-terminal.ts(container exec terminal):
- After
validateRequestgetsuser/session, in the branch whereserverIdexists, in addition to the existingserver.organizationId !== session.activeOrganizationIdcheck, thememberrecord is also queried. - If
memberRecord.roleis notowner/admin,memberRecord.canAccessToDocker === truemust 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 andcontainerId/appNamein the database schema, or the existing Docker service query function) to confirm whether the service is in thememberRecord.accessedServicesarray. If not, reject it to prevent members from exec into containers belonging to other tenants on the same shared server. - Local branches where
serverIddoes not exist (elsebranches,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 addowner/adminorcanAccessToDockerpermission verification, combined withcontainerIdownership verification.
- Supplement unit tests or integration tests to cover the following scenarios: members who are not owner/admin and do not have
canAccessToDocker/canAccessToSSHKeysenabled should attempt to connect to the terminal (ws.closeis called); members with corresponding permissions enabled andcontainerId/serverIdin 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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