From 069d3e2beb47c2b4d908e38aa588ad6c650a0c7f Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Mon, 16 Mar 2026 11:42:01 +0100 Subject: [PATCH] fix(coderd): require ssh access for workspace chats (#23094) ### Motivation - The chat creation flow associated a workspace agent for a chat if the requester could read the workspace, enabling privilege escalation where users without SSH/app-connect permissions could cause the daemon to open privileged agent connections and execute commands. - The intent is to ensure that attaching a workspace agent to a chat only happens when the requester has the workspace SSH permission so the chat daemon cannot be abused to bypass RBAC. ### Description - Require request-scoped authorization for workspace agent usage by changing `validateCreateChatWorkspaceSelection` to accept the `*http.Request` and calling `api.Authorize(r, policy.ActionSSH, workspace)` before selecting the workspace for a chat. - Pass the HTTP request into the validator from `postChats` so authorization is evaluated in the request context (`postChats` now calls `validateCreateChatWorkspaceSelection(ctx, r, req)`). - Add a regression test `WorkspaceAccessibleButNoSSH` in `coderd/chats_test.go` which creates an org-admin-scoped user (read access but no `ActionSSH`) and asserts that creating a chat with `WorkspaceID` is denied. ### Testing - Ran `gofmt -w coderd/chats.go coderd/chats_test.go` which succeeded. - Attempted to run repository pre-commit checks (`make pre-commit`) and targeted `go test` invocations; these checks could not be completed in this environment due to missing local tooling and environment constraints (protobuf include resolution, containerized DB access via Docker socket, and long-running golden generation tasks), so full CI/pre-commit verification and end-to-end test runs did not complete here. - Added a focused regression unit test (`WorkspaceAccessibleButNoSSH`) to prevent reintroduction of the authorization bypass; this test is included in the change and should be executed in CI where the full toolchain and test environment are available. ------ [Codex Task](https://chatgpt.com/codex/tasks/task_b_69b432502670832e91d14e937745de46) --- coderd/chats.go | 9 ++++++++- coderd/chats_test.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/coderd/chats.go b/coderd/chats.go index ce211c62cb..46ebc301aa 100644 --- a/coderd/chats.go +++ b/coderd/chats.go @@ -237,7 +237,7 @@ func (api *API) postChats(rw http.ResponseWriter, r *http.Request) { return } - workspaceSelection, validationStatus, validationError := api.validateCreateChatWorkspaceSelection(ctx, req) + workspaceSelection, validationStatus, validationError := api.validateCreateChatWorkspaceSelection(ctx, r, req) if validationError != nil { httpapi.Write(ctx, rw, validationStatus, *validationError) return @@ -1899,6 +1899,7 @@ type createChatWorkspaceSelection struct { func (api *API) validateCreateChatWorkspaceSelection( ctx context.Context, + r *http.Request, req codersdk.CreateChatRequest, ) ( createChatWorkspaceSelection, @@ -1927,6 +1928,12 @@ func (api *API) validateCreateChatWorkspaceSelection( Valid: true, } + if !api.Authorize(r, policy.ActionSSH, workspace) { + return selection, http.StatusBadRequest, &codersdk.Response{ + Message: "Workspace not found or you do not have access to this resource", + } + } + return selection, 0, nil } diff --git a/coderd/chats_test.go b/coderd/chats_test.go index 21d734d818..b1912a1bb5 100644 --- a/coderd/chats_test.go +++ b/coderd/chats_test.go @@ -23,6 +23,7 @@ import ( "github.com/coder/coder/v2/coderd/database/dbfake" "github.com/coder/coder/v2/coderd/externalauth" coderdpubsub "github.com/coder/coder/v2/coderd/pubsub" + "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/testutil" "github.com/coder/websocket" @@ -162,6 +163,41 @@ func TestPostChats(t *testing.T) { ) }) + t.Run("WorkspaceAccessibleButNoSSH", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitLong) + adminClient, db := newChatClientWithDatabase(t) + firstUser := coderdtest.CreateFirstUser(t, adminClient) + orgAdminClient, _ := coderdtest.CreateAnotherUser( + t, + adminClient, + firstUser.OrganizationID, + rbac.ScopedRoleOrgAdmin(firstUser.OrganizationID), + ) + + workspaceBuild := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: firstUser.OrganizationID, + OwnerID: firstUser.UserID, + }).WithAgent().Do() + + _, err := orgAdminClient.CreateChat(ctx, codersdk.CreateChatRequest{ + Content: []codersdk.ChatInputPart{ + { + Type: codersdk.ChatInputPartTypeText, + Text: "hello", + }, + }, + WorkspaceID: &workspaceBuild.Workspace.ID, + }) + sdkErr := requireSDKError(t, err, http.StatusBadRequest) + require.Equal( + t, + "Workspace not found or you do not have access to this resource", + sdkErr.Message, + ) + }) + t.Run("WorkspaceNotFound", func(t *testing.T) { t.Parallel()