High severity8.3GHSA Advisory· Published May 9, 2026· Updated May 15, 2026
CVE-2026-42297
CVE-2026-42297
Description
Argo Workflows is an open source container-native workflow engine for orchestrating parallel jobs on Kubernetes. From version 4.0.0 to before version 4.0.5, the Sync Service's ConfigMap-backed provider (server/sync/sync_cm.go) performs zero authorization checks on all CRUD operations (create, read, update, delete). Any authenticated user — including those using fake Bearer tokens — can create, read, update, and delete Kubernetes ConfigMaps containing synchronization limits. This issue has been patched in version 4.0.5.
Affected products
1- Range: >= 4.0.0, < 4.0.5
Patches
109fff05e0830Merge commit from fork
3 files changed · +150 −31
server/sync/sync_cm.go+30 −0 modified@@ -6,12 +6,14 @@ import ( "strconv" "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" syncpkg "github.com/argoproj/argo-workflows/v4/pkg/apiclient/sync" "github.com/argoproj/argo-workflows/v4/server/auth" sutils "github.com/argoproj/argo-workflows/v4/server/utils" + authutil "github.com/argoproj/argo-workflows/v4/util/auth" ) type configMapSyncProvider struct{} @@ -23,6 +25,10 @@ func (s *configMapSyncProvider) createSyncLimit(ctx context.Context, req *syncpk return nil, sutils.ToStatusError(fmt.Errorf("limit must be greater than zero"), codes.InvalidArgument) } + if err := checkConfigMapPermission(ctx, "create", req.Namespace); err != nil { + return nil, err + } + kubeClient := auth.GetKubeClient(ctx) configmapGetter := kubeClient.CoreV1().ConfigMaps(req.Namespace) @@ -67,6 +73,10 @@ func (s *configMapSyncProvider) createSyncLimit(ctx context.Context, req *syncpk } func (s *configMapSyncProvider) getSyncLimit(ctx context.Context, req *syncpkg.GetSyncLimitRequest) (*syncpkg.SyncLimitResponse, error) { + if err := checkConfigMapPermission(ctx, "get", req.Namespace); err != nil { + return nil, err + } + kubeClient := auth.GetKubeClient(ctx) configmapGetter := kubeClient.CoreV1().ConfigMaps(req.Namespace) @@ -100,10 +110,18 @@ func (s *configMapSyncProvider) updateSyncLimit(ctx context.Context, req *syncpk return nil, sutils.ToStatusError(fmt.Errorf("limit must be greater than zero"), codes.InvalidArgument) } + if err := checkConfigMapPermission(ctx, "update", req.Namespace); err != nil { + return nil, err + } + return s.handleUpdateSyncLimit(ctx, req, true) } func (s *configMapSyncProvider) deleteSyncLimit(ctx context.Context, req *syncpkg.DeleteSyncLimitRequest) (*syncpkg.DeleteSyncLimitResponse, error) { + if err := checkConfigMapPermission(ctx, "update", req.Namespace); err != nil { + return nil, err + } + kubeClient := auth.GetKubeClient(ctx) configmapGetter := kubeClient.CoreV1().ConfigMaps(req.Namespace) @@ -123,6 +141,18 @@ func (s *configMapSyncProvider) deleteSyncLimit(ctx context.Context, req *syncpk return &syncpkg.DeleteSyncLimitResponse{}, nil } +func checkConfigMapPermission(ctx context.Context, verb, namespace string) error { + kubeClient := auth.GetKubeClient(ctx) + allowed, err := authutil.CanI(ctx, kubeClient, []string{verb}, "", namespace, "configmaps") + if err != nil { + return sutils.ToStatusError(err, codes.Internal) + } + if !allowed { + return status.Error(codes.PermissionDenied, fmt.Sprintf("Permission denied, you are not allowed to %s configmaps in namespace %q.", verb, namespace)) + } + return nil +} + func (s *configMapSyncProvider) handleUpdateSyncLimit(ctx context.Context, req *syncpkg.UpdateSyncLimitRequest, shouldFieldExist bool) (*syncpkg.SyncLimitResponse, error) { kubeClient := auth.GetKubeClient(ctx)
server/sync/sync_db.go+0 −1 modified@@ -23,7 +23,6 @@ var _ SyncConfigProvider = &dbSyncProvider{} func (s *dbSyncProvider) createSyncLimit(ctx context.Context, req *syncpkg.CreateSyncLimitRequest) (*syncpkg.SyncLimitResponse, error) { // since there's no permission system for db sync limits, we use the k8s RBAC check to see if the request is reasonable - // configmap version is relying on the k8s RBAC so we don't need to check permissions allowed, err := auth.CanI(ctx, "create", workflow.WorkflowPlural, req.Namespace, "") if err != nil { return nil, sutils.ToStatusError(err, codes.Internal)
server/sync/sync_server_cm_test.go+120 −30 modified@@ -8,20 +8,36 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + authorizationv1 "k8s.io/api/authorization/v1" corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/kubernetes/fake" ktesting "k8s.io/client-go/testing" syncpkg "github.com/argoproj/argo-workflows/v4/pkg/apiclient/sync" "github.com/argoproj/argo-workflows/v4/server/auth" + "github.com/argoproj/argo-workflows/v4/util/logging" ) -func withKubeClient(kubeClient *fake.Clientset) context.Context { - return context.WithValue(context.Background(), auth.KubeKey, kubeClient) +func withAllowedKubeClient(t *testing.T, kubeClient *fake.Clientset) context.Context { + t.Helper() + kubeClient.PrependReactor("create", "selfsubjectaccessreviews", func(action ktesting.Action) (bool, runtime.Object, error) { + return true, &authorizationv1.SelfSubjectAccessReview{ + Status: authorizationv1.SubjectAccessReviewStatus{Allowed: true}, + }, nil + }) + return context.WithValue(logging.TestContext(t.Context()), auth.KubeKey, kubeClient) +} + +func withDeniedKubeClient(t *testing.T, kubeClient *fake.Clientset) context.Context { + t.Helper() + kubeClient.PrependReactor("create", "selfsubjectaccessreviews", func(action ktesting.Action) (bool, runtime.Object, error) { + return true, &authorizationv1.SelfSubjectAccessReview{ + Status: authorizationv1.SubjectAccessReviewStatus{Allowed: false}, + }, nil + }) + return context.WithValue(logging.TestContext(t.Context()), auth.KubeKey, kubeClient) } func Test_syncServer_CreateSyncLimit(t *testing.T) { @@ -45,18 +61,34 @@ func Test_syncServer_CreateSyncLimit(t *testing.T) { require.Contains(t, statusErr.Message(), "limit must be greater than zero") }) + t.Run("Permission denied", func(t *testing.T) { + kubeClient := fake.NewClientset() + ctx := withDeniedKubeClient(t, kubeClient) + server := NewSyncServer(ctx, kubeClient, "", nil) + + req := &syncpkg.CreateSyncLimitRequest{ + CmName: "test-cm", + Namespace: "test-ns", + Key: "test-key", + Limit: 100, + } + + _, err := server.CreateSyncLimit(ctx, req) + + require.Error(t, err) + statusErr, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, codes.PermissionDenied, statusErr.Code()) + }) + t.Run("Error creating ConfigMap", func(t *testing.T) { kubeClient := fake.NewSimpleClientset() kubeClient.PrependReactor("create", "configmaps", func(action ktesting.Action) (bool, runtime.Object, error) { - return true, nil, apierrors.NewForbidden( - schema.GroupResource{Group: "", Resource: "configmaps"}, - "test-cm", - errors.New("namespace not found"), - ) + return true, nil, errors.New("create error") }) - ctx := context.WithValue(context.Background(), auth.KubeKey, kubeClient) + ctx := withAllowedKubeClient(t, kubeClient) server := NewSyncServer(ctx, kubeClient, "", nil) req := &syncpkg.CreateSyncLimitRequest{ @@ -71,13 +103,13 @@ func Test_syncServer_CreateSyncLimit(t *testing.T) { require.Error(t, err) statusErr, ok := status.FromError(err) require.True(t, ok) - require.Equal(t, codes.PermissionDenied, statusErr.Code()) - require.Contains(t, statusErr.Message(), "namespace not found") + require.Equal(t, codes.Internal, statusErr.Code()) + require.Contains(t, statusErr.Message(), "create error") }) t.Run("Create new ConfigMap", func(t *testing.T) { kubeClient := fake.NewSimpleClientset() - ctx := withKubeClient(kubeClient) + ctx := withAllowedKubeClient(t, kubeClient) server := NewSyncServer(ctx, kubeClient, "", nil) req := &syncpkg.CreateSyncLimitRequest{ @@ -107,7 +139,7 @@ func Test_syncServer_CreateSyncLimit(t *testing.T) { }, } kubeClient := fake.NewSimpleClientset(existingCM) - ctx := withKubeClient(kubeClient) + ctx := withAllowedKubeClient(t, kubeClient) server := NewSyncServer(ctx, kubeClient, "", nil) req := &syncpkg.CreateSyncLimitRequest{ @@ -136,7 +168,7 @@ func Test_syncServer_CreateSyncLimit(t *testing.T) { Data: nil, } kubeClient := fake.NewSimpleClientset(existingCM) - ctx := withKubeClient(kubeClient) + ctx := withAllowedKubeClient(t, kubeClient) server := NewSyncServer(ctx, kubeClient, "", nil) req := &syncpkg.CreateSyncLimitRequest{ @@ -165,7 +197,7 @@ func Test_syncServer_CreateSyncLimit(t *testing.T) { }, } kubeClient := fake.NewSimpleClientset(existingCM) - ctx := withKubeClient(kubeClient) + ctx := withAllowedKubeClient(t, kubeClient) server := NewSyncServer(ctx, kubeClient, "", nil) req := &syncpkg.CreateSyncLimitRequest{ @@ -186,9 +218,28 @@ func Test_syncServer_CreateSyncLimit(t *testing.T) { } func Test_syncServer_GetSyncLimit(t *testing.T) { + t.Run("Permission denied", func(t *testing.T) { + kubeClient := fake.NewClientset() + ctx := withDeniedKubeClient(t, kubeClient) + server := NewSyncServer(ctx, kubeClient, "", nil) + + req := &syncpkg.GetSyncLimitRequest{ + CmName: "test-cm", + Namespace: "test-ns", + Key: "test-key", + } + + _, err := server.GetSyncLimit(ctx, req) + + require.Error(t, err) + statusErr, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, codes.PermissionDenied, statusErr.Code()) + }) + t.Run("ConfigMap doesn't exist", func(t *testing.T) { kubeClient := fake.NewSimpleClientset() - ctx := withKubeClient(kubeClient) + ctx := withAllowedKubeClient(t, kubeClient) server := NewSyncServer(ctx, kubeClient, "", nil) req := &syncpkg.GetSyncLimitRequest{ @@ -218,7 +269,7 @@ func Test_syncServer_GetSyncLimit(t *testing.T) { }, } kubeClient := fake.NewSimpleClientset(existingCM) - ctx := withKubeClient(kubeClient) + ctx := withAllowedKubeClient(t, kubeClient) server := NewSyncServer(ctx, kubeClient, "", nil) req := &syncpkg.GetSyncLimitRequest{ @@ -248,7 +299,7 @@ func Test_syncServer_GetSyncLimit(t *testing.T) { }, } kubeClient := fake.NewSimpleClientset(existingCM) - ctx := withKubeClient(kubeClient) + ctx := withAllowedKubeClient(t, kubeClient) server := NewSyncServer(ctx, kubeClient, "", nil) req := &syncpkg.GetSyncLimitRequest{ @@ -277,7 +328,7 @@ func Test_syncServer_GetSyncLimit(t *testing.T) { }, } kubeClient := fake.NewSimpleClientset(existingCM) - ctx := withKubeClient(kubeClient) + ctx := withAllowedKubeClient(t, kubeClient) server := NewSyncServer(ctx, kubeClient, "", nil) req := &syncpkg.GetSyncLimitRequest{ @@ -317,9 +368,29 @@ func Test_syncServer_UpdateSyncLimit(t *testing.T) { require.Contains(t, statusErr.Message(), "limit must be greater than zero") }) + t.Run("Permission denied", func(t *testing.T) { + kubeClient := fake.NewClientset() + ctx := withDeniedKubeClient(t, kubeClient) + server := NewSyncServer(ctx, kubeClient, "", nil) + + req := &syncpkg.UpdateSyncLimitRequest{ + CmName: "test-cm", + Namespace: "test-ns", + Key: "test-key", + Limit: 100, + } + + _, err := server.UpdateSyncLimit(ctx, req) + + require.Error(t, err) + statusErr, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, codes.PermissionDenied, statusErr.Code()) + }) + t.Run("ConfigMap doesn't exist", func(t *testing.T) { kubeClient := fake.NewSimpleClientset() - ctx := withKubeClient(kubeClient) + ctx := withAllowedKubeClient(t, kubeClient) server := NewSyncServer(ctx, kubeClient, "", nil) req := &syncpkg.UpdateSyncLimitRequest{ @@ -347,7 +418,7 @@ func Test_syncServer_UpdateSyncLimit(t *testing.T) { Data: nil, } kubeClient := fake.NewSimpleClientset(existingCM) - ctx := withKubeClient(kubeClient) + ctx := withAllowedKubeClient(t, kubeClient) server := NewSyncServer(ctx, kubeClient, "", nil) req := &syncpkg.UpdateSyncLimitRequest{ @@ -378,7 +449,7 @@ func Test_syncServer_UpdateSyncLimit(t *testing.T) { }, } kubeClient := fake.NewSimpleClientset(existingCM) - ctx := withKubeClient(kubeClient) + ctx := withAllowedKubeClient(t, kubeClient) server := NewSyncServer(ctx, kubeClient, "", nil) req := &syncpkg.UpdateSyncLimitRequest{ @@ -413,7 +484,7 @@ func Test_syncServer_UpdateSyncLimit(t *testing.T) { return true, nil, errors.New("update error") }) - ctx := withKubeClient(kubeClient) + ctx := withAllowedKubeClient(t, kubeClient) server := NewSyncServer(ctx, kubeClient, "", nil) req := &syncpkg.UpdateSyncLimitRequest{ @@ -443,7 +514,7 @@ func Test_syncServer_UpdateSyncLimit(t *testing.T) { }, } kubeClient := fake.NewSimpleClientset(existingCM) - ctx := withKubeClient(kubeClient) + ctx := withAllowedKubeClient(t, kubeClient) server := NewSyncServer(ctx, kubeClient, "", nil) req := &syncpkg.UpdateSyncLimitRequest{ @@ -464,9 +535,28 @@ func Test_syncServer_UpdateSyncLimit(t *testing.T) { } func Test_syncServer_DeleteSyncLimit(t *testing.T) { + t.Run("Permission denied", func(t *testing.T) { + kubeClient := fake.NewClientset() + ctx := withDeniedKubeClient(t, kubeClient) + server := NewSyncServer(ctx, kubeClient, "", nil) + + req := &syncpkg.DeleteSyncLimitRequest{ + CmName: "test-cm", + Namespace: "test-ns", + Key: "test-key", + } + + _, err := server.DeleteSyncLimit(ctx, req) + + require.Error(t, err) + statusErr, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, codes.PermissionDenied, statusErr.Code()) + }) + t.Run("ConfigMap doesn't exist", func(t *testing.T) { kubeClient := fake.NewSimpleClientset() - ctx := withKubeClient(kubeClient) + ctx := withAllowedKubeClient(t, kubeClient) server := NewSyncServer(ctx, kubeClient, "", nil) req := &syncpkg.DeleteSyncLimitRequest{ @@ -493,7 +583,7 @@ func Test_syncServer_DeleteSyncLimit(t *testing.T) { Data: nil, } kubeClient := fake.NewSimpleClientset(existingCM) - ctx := withKubeClient(kubeClient) + ctx := withAllowedKubeClient(t, kubeClient) server := NewSyncServer(ctx, kubeClient, "", nil) req := &syncpkg.DeleteSyncLimitRequest{ @@ -516,7 +606,7 @@ func Test_syncServer_DeleteSyncLimit(t *testing.T) { Data: map[string]string{}, } kubeClient := fake.NewSimpleClientset(existingCM) - ctx := withKubeClient(kubeClient) + ctx := withAllowedKubeClient(t, kubeClient) server := NewSyncServer(ctx, kubeClient, "", nil) req := &syncpkg.DeleteSyncLimitRequest{ @@ -546,7 +636,7 @@ func Test_syncServer_DeleteSyncLimit(t *testing.T) { return true, nil, errors.New("update error") }) - ctx := withKubeClient(kubeClient) + ctx := withAllowedKubeClient(t, kubeClient) server := NewSyncServer(ctx, kubeClient, "", nil) req := &syncpkg.DeleteSyncLimitRequest{ @@ -576,7 +666,7 @@ func Test_syncServer_DeleteSyncLimit(t *testing.T) { }, } kubeClient := fake.NewSimpleClientset(existingCM) - ctx := withKubeClient(kubeClient) + ctx := withAllowedKubeClient(t, kubeClient) server := NewSyncServer(ctx, kubeClient, "", nil) req := &syncpkg.DeleteSyncLimitRequest{
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/argoproj/argo-workflows/commit/09fff05e0830c14a5e36cc40597ad84881db1ab6nvdPatch
- github.com/argoproj/argo-workflows/security/advisories/GHSA-xchc-cqwg-g76qnvdExploitVendor Advisory
- github.com/advisories/GHSA-xchc-cqwg-g76qghsaADVISORY
- github.com/argoproj/argo-workflows/releases/tag/v4.0.5nvdRelease Notes
- nvd.nist.gov/vuln/detail/CVE-2026-42297ghsa
News mentions
0No linked articles in our index yet.