CVE-2024-27916 (published 2024-03-21T02:52Z)

Minder is a software supply chain security platform. Prior to version 0.0.33, a Minder user can use the endpoints `GetRepositoryByName`, `DeleteRepositoryByName`, and `GetArtifactByName` to access any repository in the database, irrespective of who owns the repo and any permissions present. The database query checks by repo owner, repo name and provider name (which is always `github`). These query values are not distinct for the particular user - as long as the user has valid credentials and a provider, they can set the repo owner/name to any value they want and the server will return information on this repo. Version 0.0.33 contains a patch for this issue.


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/stacklok/minder
commit_details: repo_url: https://github.com/stacklok/minder
author: Don Browne
author_date: 2024-03-04 16:39:51+00:00
msg: Merge pull request from GHSA-v627-69v2-xx37

Fixes #GHSA-v627-69v2-xx37

Change GetRepositoryByRepoName query to include project ID. This enforces that
the user can only access the repos in the project they authorized with.
This fixes a security issue where several API endpoints allowed any
authenticated user to manipulate any repo in the DB, irrespective of who
owned it.
repository: repo_url: https://github.com/stacklok/minder
repo_name: stacklok/minder
description: Software Supply Chain Security Platform
repo_language: Go
owner: stacklok
file_changes: filename: repositories.sql
change_type: ModificationType.MODIFY
diff: @@ -20,7 +20,7 @@ SELECT * FROM repositories WHERE id = $1;
 SELECT * FROM repositories WHERE repo_id = $1;
 
 -- name: GetRepositoryByRepoName :one
-SELECT * FROM repositories WHERE provider = $1 AND repo_owner = $2 AND repo_name = $3;
+SELECT * FROM repositories WHERE provider = $1 AND repo_owner = $2 AND repo_name = $3 AND project_id = $4;
 
 -- name: GetRepositoryByIDAndProject :one
 SELECT * FROM repositories WHERE provider = $1 AND repo_id = $2 AND project_id = $3;

programming_language: SQL
filename: handlers_artifacts.go
change_type: ModificationType.MODIFY
diff: @@ -71,10 +71,14 @@ func (s *Server) GetArtifactByName(ctx context.Context, in *pb.GetArtifactByName
 		return nil, util.UserVisibleError(codes.InvalidArgument, "invalid artifact name user repoOwner/repoName/artifactName")
 	}
 
+	entityCtx := engine.EntityFromContext(ctx)
+	projectID := entityCtx.Project.ID
+
 	repo, err := s.store.GetRepositoryByRepoName(ctx, db.GetRepositoryByRepoNameParams{
 		Provider:  in.GetContext().GetProvider(),
 		RepoOwner: nameParts[0],
 		RepoName:  nameParts[1],
+		ProjectID: projectID,
 	})
 	if err != nil {
 		if errors.Is(err, sql.ErrNoRows) {

programming_language: Go
filename: handlers_repositories.go
change_type: ModificationType.MODIFY
diff: @@ -274,8 +274,12 @@ func (s *Server) GetRepositoryByName(ctx context.Context,
 		return nil, providerError(err)
 	}
 
-	repo, err := s.store.GetRepositoryByRepoName(ctx,
-		db.GetRepositoryByRepoNameParams{Provider: provider.Name, RepoOwner: fragments[0], RepoName: fragments[1]})
+	repo, err := s.store.GetRepositoryByRepoName(ctx, db.GetRepositoryByRepoNameParams{
+		Provider:  provider.Name,
+		RepoOwner: fragments[0],
+		RepoName:  fragments[1],
+		ProjectID: projectID,
+	})
 
 	if errors.Is(err, sql.ErrNoRows) {
 		return nil, status.Errorf(codes.NotFound, "repository not found")
@@ -352,8 +356,12 @@ func (s *Server) DeleteRepositoryByName(ctx context.Context,
 		return nil, providerError(err)
 	}
 
-	repo, err := s.store.GetRepositoryByRepoName(ctx,
-		db.GetRepositoryByRepoNameParams{Provider: provider.Name, RepoOwner: fragments[0], RepoName: fragments[1]})
+	repo, err := s.store.GetRepositoryByRepoName(ctx, db.GetRepositoryByRepoNameParams{
+		Provider:  provider.Name,
+		RepoOwner: fragments[0],
+		RepoName:  fragments[1],
+		ProjectID: projectID,
+	})
 
 	if errors.Is(err, sql.ErrNoRows) {
 		return nil, status.Errorf(codes.NotFound, "repository not found")

programming_language: Go
filename: repositories.sql.go
change_type: ModificationType.MODIFY
diff: @@ -188,17 +188,23 @@ func (q *Queries) GetRepositoryByRepoID(ctx context.Context, repoID int64) (Repo
 }
 
 const getRepositoryByRepoName = `-- name: GetRepositoryByRepoName :one
-SELECT id, provider, project_id, repo_owner, repo_name, repo_id, is_private, is_fork, webhook_id, webhook_url, deploy_url, clone_url, created_at, updated_at, default_branch FROM repositories WHERE provider = $1 AND repo_owner = $2 AND repo_name = $3
+SELECT id, provider, project_id, repo_owner, repo_name, repo_id, is_private, is_fork, webhook_id, webhook_url, deploy_url, clone_url, created_at, updated_at, default_branch FROM repositories WHERE provider = $1 AND repo_owner = $2 AND repo_name = $3 AND project_id = $4
 `
 
 type GetRepositoryByRepoNameParams struct {
-	Provider  string `json:"provider"`
-	RepoOwner string `json:"repo_owner"`
-	RepoName  string `json:"repo_name"`
+	Provider  string    `json:"provider"`
+	RepoOwner string    `json:"repo_owner"`
+	RepoName  string    `json:"repo_name"`
+	ProjectID uuid.UUID `json:"project_id"`
 }
 
 func (q *Queries) GetRepositoryByRepoName(ctx context.Context, arg GetRepositoryByRepoNameParams) (Repository, error) {
-	row := q.db.QueryRowContext(ctx, getRepositoryByRepoName, arg.Provider, arg.RepoOwner, arg.RepoName)
+	row := q.db.QueryRowContext(ctx, getRepositoryByRepoName,
+		arg.Provider,
+		arg.RepoOwner,
+		arg.RepoName,
+		arg.ProjectID,
+	)
 	var i Repository
 	err := row.Scan(
 		&i.ID,

programming_language: Go
filename: repositories_test.go
change_type: ModificationType.MODIFY
diff: @@ -129,7 +129,11 @@ func TestGetRepositoryByRepoName(t *testing.T) {
 	repo1 := createRandomRepository(t, project.ID, prov.Name)
 
 	repo2, err := testQueries.GetRepositoryByRepoName(context.Background(), GetRepositoryByRepoNameParams{
-		Provider: repo1.Provider, RepoOwner: repo1.RepoOwner, RepoName: repo1.RepoName})
+		Provider:  repo1.Provider,
+		RepoOwner: repo1.RepoOwner,
+		RepoName:  repo1.RepoName,
+		ProjectID: project.ID,
+	})
 	require.NoError(t, err)
 	require.NotEmpty(t, repo2)
 

programming_language: Go