fix(coderd/workspaceapps): verify workspace owner matches app username (#26085)

fix(coderd/workspaceapps): verify workspace owner matches app username

When resolving a workspace app by workspace UUID, the URL's username
segment was never reconciled against the resolved workspace's owner.
A user could serve their own workspace app from a hostname embedding
another user's username, so the parsed origin username belonged to the
victim.  Combined with the username-equality CORS check, this allowed
credentialed cross-origin reads of the victim's app responses.

Reject the request with a 404 when the resolved workspace's owner does
not match the user named in the request.

Refs: https://linear.app/codercom/issue/PLAT-260
This commit is contained in:
George K
2026-06-10 16:26:45 -07:00
committed by GitHub
parent 77522c3945
commit 0b99e67ce7
3 changed files with 75 additions and 0 deletions
+28
View File
@@ -1247,6 +1247,34 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
assertWorkspaceLastUsedAtNotUpdated(t, appDetails, testutil.WaitLong)
})
// Security (PLAT-260): must 404 when the URL username segment
// names a different owner than the resolved workspace.
t.Run("WorkspaceUUIDOwnerMismatchShould404", func(t *testing.T) {
t.Parallel()
appDetails := setupProxyTest(t, nil)
otherUserClient, otherUser := coderdtest.CreateAnotherUser(t, appDetails.SDKClient, appDetails.FirstUser.OrganizationID, rbac.RoleMember())
appClient := appDetails.AppClient(t)
appClient.SetSessionToken(otherUserClient.SessionToken())
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
forgedApp := appDetails.Apps.Public
forgedApp.Username = otherUser.Username
forgedApp.WorkspaceName = appDetails.Workspace.ID.String()
resp, err := requestWithRetries(ctx, t, appClient, http.MethodGet, appDetails.SubdomainAppURL(forgedApp).String(), nil)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusNotFound, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Contains(t, string(body), "404 - Application Not Found")
assertWorkspaceLastUsedAtNotUpdated(t, appDetails, testutil.WaitLong)
})
t.Run("RedirectsWithSlash", func(t *testing.T) {
t.Parallel()