CVE-2022-31155 (published 2022-08-01T19:15Z)

Sourcegraph is an opensource code search and navigation engine. In Sourcegraph versions before 3.41.0, it is possible for an attacker to delete other users’ saved searches due to a bug in the authorization check. The vulnerability does not allow the reading of other users’ saved searches, only overwriting them with attacker-controlled searches. The issue is patched in Sourcegraph version 3.41.0. There is no workaround for this issue and updating to a secure version is highly recommended.

CVSS v3 base score: 4.3, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N)

Weakness information:
cwe_id: CWE-863
cwe_name: Incorrect Authorization
description: The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. This allows attackers to bypass intended access restrictions.
extended_description: 
            Assuming a user with a given identity, authorization is the process of determining whether that user can access a given resource, based on the user's privileges and any permissions or other access-control specifications that apply to the resource.
            When access control checks are incorrectly applied, users are able to access data or perform actions that they should not be allowed to perform. This can lead to a wide range of problems, including information exposures, denial of service, and arbitrary code execution.
         
         

Fix information:
repo_url: https://github.com/sourcegraph/sourcegraph
commit_details: repo_url: https://github.com/sourcegraph/sourcegraph
author: Camden Cheek
author_date: 2022-06-06 14:57:04-06:00
msg: Saved searches: fix update permissions (#36674)
repository: repo_url: https://github.com/sourcegraph/sourcegraph
repo_name: sourcegraph/sourcegraph
description: Code AI platform with Code Search & Cody
repo_language: Go
owner: sourcegraph
file_changes: filename: CHANGELOG.md
change_type: ModificationType.MODIFY
diff: @@ -33,6 +33,7 @@ All notable changes to Sourcegraph are documented in this file.
 ### Fixed
 
 - A common source of searcher evictions on kubernetes when running large structural searches. [#34828](https://github.com/sourcegraph/sourcegraph/issues/34828)
+- An issue with permissions evaluation for saved searches
 
 ### Removed
 

programming_language: Markdown
filename: saved_searches.go
change_type: ModificationType.MODIFY
diff: @@ -196,35 +196,29 @@ func (r *schemaResolver) UpdateSavedSearch(ctx context.Context, args *struct {
 	OrgID       *graphql.ID
 	UserID      *graphql.ID
 }) (*savedSearchResolver, error) {
-	var userID, orgID *int32
+	id, err := unmarshalSavedSearchID(args.ID)
+	if err != nil {
+		return nil, err
+	}
+
+	old, err := r.db.SavedSearches().GetByID(ctx, id)
+	if err != nil {
+		return nil, errors.Wrap(err, "fetch old saved search")
+	}
+
 	// 🚨 SECURITY: Make sure the current user has permission to update a saved search for the specified user or org.
-	if args.UserID != nil {
-		u, err := unmarshalSavedSearchID(*args.UserID)
-		if err != nil {
-			return nil, err
-		}
-		userID = &u
-		if err := backend.CheckSiteAdminOrSameUser(ctx, r.db, u); err != nil {
-			return nil, err
-		}
-	} else if args.OrgID != nil {
-		o, err := unmarshalSavedSearchID(*args.OrgID)
-		if err != nil {
+	if old.Config.UserID != nil {
+		if err := backend.CheckSiteAdminOrSameUser(ctx, r.db, *old.Config.UserID); err != nil {
 			return nil, err
 		}
-		orgID = &o
-		if err := backend.CheckOrgAccessOrSiteAdmin(ctx, r.db, o); err != nil {
+	} else if old.Config.OrgID != nil {
+		if err := backend.CheckOrgAccessOrSiteAdmin(ctx, r.db, *old.Config.OrgID); err != nil {
 			return nil, err
 		}
 	} else {
 		return nil, errors.New("failed to update saved search: no Org ID or User ID associated with saved search")
 	}
 
-	id, err := unmarshalSavedSearchID(args.ID)
-	if err != nil {
-		return nil, err
-	}
-
 	if !queryHasPatternType(args.Query) {
 		return nil, errMissingPatternType
 	}
@@ -235,8 +229,8 @@ func (r *schemaResolver) UpdateSavedSearch(ctx context.Context, args *struct {
 		Query:       args.Query,
 		Notify:      args.NotifyOwner,
 		NotifySlack: args.NotifySlack,
-		UserID:      userID,
-		OrgID:       orgID,
+		UserID:      old.Config.UserID,
+		OrgID:       old.Config.OrgID,
 	})
 	if err != nil {
 		return nil, err

programming_language: Go
filename: saved_searches_test.go
change_type: ModificationType.MODIFY
diff: @@ -7,7 +7,9 @@ import (
 
 	mockrequire "github.com/derision-test/go-mockgen/testutil/require"
 	"github.com/graph-gophers/graphql-go"
+	"github.com/stretchr/testify/require"
 
+	"github.com/sourcegraph/sourcegraph/cmd/frontend/backend"
 	"github.com/sourcegraph/sourcegraph/internal/actor"
 	"github.com/sourcegraph/sourcegraph/internal/api"
 	"github.com/sourcegraph/sourcegraph/internal/database"
@@ -219,6 +221,11 @@ func TestUpdateSavedSearch(t *testing.T) {
 			OrgID:       savedSearch.OrgID,
 		}, nil
 	})
+	ss.GetByIDFunc.SetDefaultReturn(&api.SavedQuerySpecAndConfig{
+		Config: api.ConfigSavedQuery{
+			UserID: &key,
+		},
+	}, nil)
 
 	db := database.NewMockDB()
 	db.UsersFunc.SetDefaultReturn(users)
@@ -233,7 +240,13 @@ func TestUpdateSavedSearch(t *testing.T) {
 		NotifySlack bool
 		OrgID       *graphql.ID
 		UserID      *graphql.ID
-	}{ID: marshalSavedSearchID(key), Description: "updated query description", Query: "test type:diff patternType:regexp", OrgID: nil, UserID: &userID})
+	}{
+		ID:          marshalSavedSearchID(key),
+		Description: "updated query description",
+		Query:       "test type:diff patternType:regexp",
+		OrgID:       nil,
+		UserID:      &userID,
+	})
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -267,6 +280,106 @@ func TestUpdateSavedSearch(t *testing.T) {
 	}
 }
 
+func TestUpdateSavedSearchPermissions(t *testing.T) {
+	user1 := &types.User{ID: 42}
+	user2 := &types.User{ID: 43}
+	admin := &types.User{ID: 44, SiteAdmin: true}
+	org1 := &types.Org{ID: 42}
+	org2 := &types.Org{ID: 43}
+
+	cases := []struct {
+		execUser *types.User
+		ssUserID *int32
+		ssOrgID  *int32
+		errIs    error
+	}{{
+		execUser: user1,
+		ssUserID: &user1.ID,
+		errIs:    nil,
+	}, {
+		execUser: user1,
+		ssUserID: &user2.ID,
+		errIs:    &backend.InsufficientAuthorizationError{},
+	}, {
+		execUser: user1,
+		ssOrgID:  &org1.ID,
+		errIs:    nil,
+	}, {
+		execUser: user1,
+		ssOrgID:  &org2.ID,
+		errIs:    backend.ErrNotAnOrgMember,
+	}, {
+		execUser: admin,
+		ssOrgID:  &user1.ID,
+		errIs:    nil,
+	}, {
+		execUser: admin,
+		ssOrgID:  &org1.ID,
+		errIs:    nil,
+	}}
+
+	for _, tt := range cases {
+		t.Run("", func(t *testing.T) {
+			ctx := actor.WithActor(context.Background(), actor.FromUser(tt.execUser.ID))
+			users := database.NewMockUserStore()
+			users.GetByCurrentAuthUserFunc.SetDefaultHook(func(ctx context.Context) (*types.User, error) {
+				switch actor.FromContext(ctx).UID {
+				case user1.ID:
+					return user1, nil
+				case user2.ID:
+					return user2, nil
+				case admin.ID:
+					return admin, nil
+				default:
+					panic("bad actor")
+				}
+			})
+
+			savedSearches := database.NewMockSavedSearchStore()
+			savedSearches.UpdateFunc.SetDefaultHook(func(_ context.Context, ss *types.SavedSearch) (*types.SavedSearch, error) {
+				return ss, nil
+			})
+			savedSearches.GetByIDFunc.SetDefaultReturn(&api.SavedQuerySpecAndConfig{
+				Config: api.ConfigSavedQuery{
+					UserID: tt.ssUserID,
+					OrgID:  tt.ssOrgID,
+				},
+			}, nil)
+
+			orgMembers := database.NewMockOrgMemberStore()
+			orgMembers.GetByOrgIDAndUserIDFunc.SetDefaultHook(func(_ context.Context, orgID int32, userID int32) (*types.OrgMembership, error) {
+				if orgID == userID {
+					return &types.OrgMembership{}, nil
+				}
+				return nil, nil
+			})
+
+			db := database.NewMockDB()
+			db.UsersFunc.SetDefaultReturn(users)
+			db.SavedSearchesFunc.SetDefaultReturn(savedSearches)
+			db.OrgMembersFunc.SetDefaultReturn(orgMembers)
+
+			_, err := (&schemaResolver{db: db}).UpdateSavedSearch(ctx, &struct {
+				ID          graphql.ID
+				Description string
+				Query       string
+				NotifyOwner bool
+				NotifySlack bool
+				OrgID       *graphql.ID
+				UserID      *graphql.ID
+			}{
+				ID:    marshalSavedSearchID(1),
+				Query: "patterntype:literal",
+			})
+			if tt.errIs == nil {
+				require.NoError(t, err)
+			} else {
+				require.ErrorAs(t, err, &tt.errIs)
+			}
+		})
+	}
+}
+
 func TestDeleteSavedSearch(t *testing.T) {
 	ctx := context.Background()
 

programming_language: Go