Skip to content

Commit 3019613

Browse files
jdomeracki-codergeokatf0ssel
authored
fix(coderd/workspaceapps): verify workspace owner matches app username (#26085) (#26261)
Cherry-pick backport to `release/2.34`. Co-authored-by: George K <george@coder.com> Co-authored-by: Garrett Delfosse <delfossegarrett@gmail.com>
1 parent e822677 commit 3019613

3 files changed

Lines changed: 75 additions & 0 deletions

File tree

coderd/workspaceapps/apptest/apptest.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,6 +1247,34 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
12471247
assertWorkspaceLastUsedAtNotUpdated(t, appDetails, testutil.WaitLong)
12481248
})
12491249

1250+
// Security (PLAT-260): must 404 when the URL username segment
1251+
// names a different owner than the resolved workspace.
1252+
t.Run("WorkspaceUUIDOwnerMismatchShould404", func(t *testing.T) {
1253+
t.Parallel()
1254+
1255+
appDetails := setupProxyTest(t, nil)
1256+
otherUserClient, otherUser := coderdtest.CreateAnotherUser(t, appDetails.SDKClient, appDetails.FirstUser.OrganizationID, rbac.RoleMember())
1257+
appClient := appDetails.AppClient(t)
1258+
appClient.SetSessionToken(otherUserClient.SessionToken())
1259+
1260+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
1261+
defer cancel()
1262+
1263+
forgedApp := appDetails.Apps.Public
1264+
forgedApp.Username = otherUser.Username
1265+
forgedApp.WorkspaceName = appDetails.Workspace.ID.String()
1266+
1267+
resp, err := requestWithRetries(ctx, t, appClient, http.MethodGet, appDetails.SubdomainAppURL(forgedApp).String(), nil)
1268+
require.NoError(t, err)
1269+
defer resp.Body.Close()
1270+
require.Equal(t, http.StatusNotFound, resp.StatusCode)
1271+
1272+
body, err := io.ReadAll(resp.Body)
1273+
require.NoError(t, err)
1274+
require.Contains(t, string(body), "404 - Application Not Found")
1275+
assertWorkspaceLastUsedAtNotUpdated(t, appDetails, testutil.WaitLong)
1276+
})
1277+
12501278
t.Run("RedirectsWithSlash", func(t *testing.T) {
12511279
t.Parallel()
12521280

coderd/workspaceapps/db_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,50 @@ func Test_ResolveRequest(t *testing.T) {
908908
require.Len(t, connLogger.ConnectionLogs(), 0)
909909
})
910910

911+
// Security (PLAT-260): a UUID workspace lookup must reject when
912+
// the URL's username segment names a different owner. Otherwise a
913+
// same-owner origin can be spoofed for credentialed cross-origin
914+
// reads.
915+
t.Run("WorkspaceUUIDOwnerMismatch", func(t *testing.T) {
916+
t.Parallel()
917+
918+
req := (workspaceapps.Request{
919+
AccessMethod: workspaceapps.AccessMethodPath,
920+
BasePath: "/app",
921+
UsernameOrID: secondUser.Username,
922+
WorkspaceNameOrID: workspace.ID.String(),
923+
AgentNameOrID: agentName,
924+
AppSlugOrPort: appNamePublic,
925+
}).Normalize()
926+
927+
connLogger := connectionlog.NewFake()
928+
auditableIP := testutil.RandomIPv6(t)
929+
930+
rw := httptest.NewRecorder()
931+
r := httptest.NewRequest("GET", "/app", nil)
932+
r.Header.Set(codersdk.SessionTokenHeader, client.SessionToken())
933+
r.RemoteAddr = auditableIP
934+
935+
token, ok := workspaceappsResolveRequest(t, connLogger, rw, r, workspaceapps.ResolveRequestOptions{
936+
Logger: api.Logger,
937+
SignedTokenProvider: api.WorkspaceAppsProvider,
938+
DashboardURL: api.AccessURL,
939+
PathAppBaseURL: api.AccessURL,
940+
AppHostname: api.AppHostname,
941+
AppRequest: req,
942+
})
943+
require.False(t, ok)
944+
require.Nil(t, token)
945+
946+
w := rw.Result()
947+
defer w.Body.Close()
948+
b, err := io.ReadAll(w.Body)
949+
require.NoError(t, err)
950+
require.Contains(t, string(b), "404 - Application Not Found")
951+
require.Equal(t, http.StatusNotFound, w.StatusCode)
952+
require.Len(t, connLogger.ConnectionLogs(), 0)
953+
})
954+
911955
t.Run("RedirectSubdomainAuth", func(t *testing.T) {
912956
t.Parallel()
913957

coderd/workspaceapps/request.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,9 @@ func (r Request) getDatabase(ctx context.Context, db database.Store) (*databaseR
248248
)
249249
if workspaceID, uuidErr := uuid.Parse(r.WorkspaceNameOrID); uuidErr == nil {
250250
workspace, workspaceErr = db.GetWorkspaceByID(ctx, workspaceID)
251+
if workspaceErr == nil && workspace.OwnerID != user.ID {
252+
workspaceErr = sql.ErrNoRows
253+
}
251254
} else {
252255
workspace, workspaceErr = db.GetWorkspaceByOwnerIDAndName(ctx, database.GetWorkspaceByOwnerIDAndNameParams{
253256
OwnerID: user.ID,

0 commit comments

Comments
 (0)