CVE-2024-36106 (Go, published 2024-06-06T15:15Z)
Description: value: Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes. It’s possible for authenticated users to enumerate clusters by name by inspecting error messages. It’s also possible to enumerate the names of projects with project-scoped clusters if you know the names of the clusters. This vulnerability is fixed in 2.11.3, 2.10.12, and 2.9.17.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/argoproj/argo-cd
Files changed:
server/cluster/cluster.go
server/cluster/cluster_test.go
Commit message: Merge pull request from GHSA-3cqf-953p-h5cp

* fix: prevent enumerating by cluster name, return exact error for case when cluster exists and not

Signed-off-by: pashakostohrys <pavel@codefresh.io>

* fix: prevent cluster enumeration by name

Signed-off-by: pashakostohrys <pavel@codefresh.io>

* fix: prevent cluster enumeration by name

Signed-off-by: pashakostohrys <pavel@codefresh.io>

* fix linter and add unit test

Signed-off-by: pashakostohrys <pavel@codefresh.io>

* fix linter and add unit test

Signed-off-by: pashakostohrys <pavel@codefresh.io>

* fix linter and add unit test

Signed-off-by: pashakostohrys <pavel@codefresh.io>

* fix linter and add unit test

Signed-off-by: pashakostohrys <pavel@codefresh.io>

* fix linter and add unit test

Signed-off-by: pashakostohrys <pavel@codefresh.io>

---------

Signed-off-by: pashakostohrys <pavel@codefresh.io>

Vulnerable code:
	c, err := s.getClusterWith403IfNotExist(ctx, q)
	if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceClusters, rbacpolicy.ActionGet, CreateClusterRBACObject(c.Project, q.Server)); err != nil {
		return nil, err
	}

	c, err := s.getClusterWith403IfNotExist(ctx, &cluster.ClusterQuery{
	})
	// verify that user can do update inside project where cluster is located
	if !s.enf.Enforce(ctx.Value("claims"), rbacpolicy.ResourceClusters, rbacpolicy.ActionUpdate, CreateClusterRBACObject(c.Project, c.Server)) {
		return nil, common.PermissionDeniedAPIError
	}

			return nil, err
		return err
			return nil, status.Errorf(codes.NotFound, "failed to get cluster servers by name: %v", err)
				return nil, status.Errorf(codes.PermissionDenied, "encountered permissions issue while processing request: %v", err)
			return nil, status.Errorf(codes.PermissionDenied, "encountered permissions issue while processing request: %v", err)
	cls, err := s.getClusterWith403IfNotExist(ctx, q)
	if err := s.enf.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceClusters, rbacpolicy.ActionUpdate, CreateClusterRBACObject(cls.Project, q.Server)); err != nil {
		return nil, err
	}

Fixed code:
	c, err := s.getClusterAndVerifyAccess(ctx, q, rbacpolicy.ActionGet)
func (s *Server) getClusterAndVerifyAccess(ctx context.Context, q *cluster.ClusterQuery, action string) (*appv1.Cluster, error) {
	c, err := s.getClusterWith403IfNotExist(ctx, q)
	if err != nil {
		return nil, err
	}

	// verify that user can do the specified action inside project where cluster is located
	if !s.enf.Enforce(ctx.Value("claims"), rbacpolicy.ResourceClusters, action, CreateClusterRBACObject(c.Project, c.Server)) {
		log.WithField("cluster", q.Server).Warnf("encountered permissions issue while processing request: %v", err)
		return nil, common.PermissionDeniedAPIError
	}

	return c, nil
}

	c, err := s.getClusterAndVerifyAccess(ctx, &cluster.ClusterQuery{
	}, rbacpolicy.ActionUpdate)

			log.WithField("cluster", q.Name).Warnf("failed to get cluster servers by name: %v", err)
			return nil, common.PermissionDeniedAPIError
		log.WithField("cluster", server).Warnf("encountered permissions issue while processing request: %v", err)
		return common.PermissionDeniedAPIError
			log.WithField("cluster", q.Name).Warnf("failed to get cluster servers by name: %v", err)
			return nil, common.PermissionDeniedAPIError
				log.WithField("cluster", server).Warnf("encountered permissions issue while processing request: %v", err)
				return nil, common.PermissionDeniedAPIError
			log.WithField("cluster", q.Server).Warnf("encountered permissions issue while processing request: %v", err)
			return nil, common.PermissionDeniedAPIError
	cls, err := s.getClusterAndVerifyAccess(ctx, q, rbacpolicy.ActionUpdate)
	"github.com/argoproj/argo-cd/v2/server/rbacpolicy"
	"github.com/argoproj/argo-cd/v2/util/assets"
	"github.com/golang-jwt/jwt/v4"
func newEnforcer() *rbac.Enforcer {
	enforcer := rbac.NewEnforcer(fake.NewSimpleClientset(test.NewFakeConfigMap()), test.FakeArgoCDNamespace, common.ArgoCDRBACConfigMapName, nil)
	_ = enforcer.SetBuiltinPolicy(assets.BuiltinPolicyCSV)
	enforcer.SetDefaultRole("role:test")
	enforcer.SetClaimsEnforcerFunc(func(claims jwt.Claims, rvals ...interface{}) bool {
		return true
	})
	return enforcer
}


func TestGetClusterAndVerifyAccess(t *testing.T) {
	t.Run("GetClusterAndVerifyAccess - No Cluster", func(t *testing.T) {
		db := &dbmocks.ArgoDB{}

		mockCluster := v1alpha1.Cluster{
			Name:       "test/ing",
			Server:     "https://127.0.0.1",
			Namespaces: []string{"default", "kube-system"},
		}
		mockClusterList := v1alpha1.ClusterList{
			ListMeta: v1.ListMeta{},
			Items: []v1alpha1.Cluster{
				mockCluster,
			},
		}

		db.On("ListClusters", mock.Anything).Return(&mockClusterList, nil)

		server := NewServer(db, newNoopEnforcer(), newServerInMemoryCache(), &kubetest.MockKubectlCmd{})
		cluster, err := server.getClusterAndVerifyAccess(context.Background(), &clusterapi.ClusterQuery{
			Name: "test/not-exists",
		}, rbacpolicy.ActionGet)

		assert.Nil(t, cluster)
		assert.ErrorIs(t, err, common.PermissionDeniedAPIError)
	})

	t.Run("GetClusterAndVerifyAccess - Permissions Denied", func(t *testing.T) {
		db := &dbmocks.ArgoDB{}

		mockCluster := v1alpha1.Cluster{
			Name:       "test/ing",
			Server:     "https://127.0.0.1",
			Namespaces: []string{"default", "kube-system"},
		}
		mockClusterList := v1alpha1.ClusterList{
			ListMeta: v1.ListMeta{},
			Items: []v1alpha1.Cluster{
				mockCluster,
			},
		}

		db.On("ListClusters", mock.Anything).Return(&mockClusterList, nil)

		server := NewServer(db, newEnforcer(), newServerInMemoryCache(), &kubetest.MockKubectlCmd{})
		cluster, err := server.getClusterAndVerifyAccess(context.Background(), &clusterapi.ClusterQuery{
			Name: "test/ing",
		}, rbacpolicy.ActionGet)

		assert.Nil(t, cluster)
		assert.ErrorIs(t, err, common.PermissionDeniedAPIError)
	})
}

func TestNoClusterEnumeration(t *testing.T) {
	db := &dbmocks.ArgoDB{}

	mockCluster := v1alpha1.Cluster{
		Name:       "test/ing",
		Server:     "https://127.0.0.1",
		Namespaces: []string{"default", "kube-system"},
	}
	mockClusterList := v1alpha1.ClusterList{
		ListMeta: v1.ListMeta{},
		Items: []v1alpha1.Cluster{
			mockCluster,
		},
	}

	db.On("ListClusters", mock.Anything).Return(&mockClusterList, nil)
	db.On("GetCluster", mock.Anything, mock.Anything).Return(&mockCluster, nil)

	server := NewServer(db, newEnforcer(), newServerInMemoryCache(), &kubetest.MockKubectlCmd{})

	t.Run("Get", func(t *testing.T) {
		_, err := server.Get(context.Background(), &clusterapi.ClusterQuery{
			Name: "cluster-not-exists",
		})
		assert.Error(t, err)
		assert.Equal(t, common.PermissionDeniedAPIError.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about cluster existence")

		_, err = server.Get(context.Background(), &clusterapi.ClusterQuery{
			Name: "test/ing",
		})
		assert.Error(t, err)
		assert.Equal(t, common.PermissionDeniedAPIError.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about cluster existence")
	})

	t.Run("Update", func(t *testing.T) {
		_, err := server.Update(context.Background(), &clusterapi.ClusterUpdateRequest{
			Cluster: &v1alpha1.Cluster{
				Name: "cluster-not-exists",
			},
		})
		assert.Error(t, err)
		assert.Equal(t, common.PermissionDeniedAPIError.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about cluster existence")

		_, err = server.Update(context.Background(), &clusterapi.ClusterUpdateRequest{
			Cluster: &v1alpha1.Cluster{
				Name: "test/ing",
			},
		})
		assert.Error(t, err)
		assert.Equal(t, common.PermissionDeniedAPIError.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about cluster existence")
	})

	t.Run("Delete", func(t *testing.T) {
		_, err := server.Delete(context.Background(), &clusterapi.ClusterQuery{
			Server: "https://127.0.0.2",
		})
		assert.Error(t, err)
		assert.Equal(t, common.PermissionDeniedAPIError.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about cluster existence")

		_, err = server.Delete(context.Background(), &clusterapi.ClusterQuery{
			Server: "https://127.0.0.1",
		})
		assert.Error(t, err)
		assert.Equal(t, common.PermissionDeniedAPIError.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about cluster existence")
	})

	t.Run("RotateAuth", func(t *testing.T) {
		_, err := server.RotateAuth(context.Background(), &clusterapi.ClusterQuery{
			Server: "https://127.0.0.2",
		})
		assert.Error(t, err)
		assert.Equal(t, common.PermissionDeniedAPIError.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about cluster existence")

		_, err = server.RotateAuth(context.Background(), &clusterapi.ClusterQuery{
			Server: "https://127.0.0.1",
		})
		assert.Error(t, err)
		assert.Equal(t, common.PermissionDeniedAPIError.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about cluster existence")
	})

	t.Run("InvalidateCache", func(t *testing.T) {
		_, err := server.InvalidateCache(context.Background(), &clusterapi.ClusterQuery{
			Server: "https://127.0.0.2",
		})
		assert.Error(t, err)
		assert.Equal(t, common.PermissionDeniedAPIError.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about cluster existence")

		_, err = server.InvalidateCache(context.Background(), &clusterapi.ClusterQuery{
			Server: "https://127.0.0.1",
		})
		assert.Error(t, err)
		assert.Equal(t, common.PermissionDeniedAPIError.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about cluster existence")
	})
}