CVE-2026-42296
Description
Argo Workflows is an open source container-native workflow engine for orchestrating parallel jobs on Kubernetes. Prior to versions 3.7.14 and 4.0.5, a user with create Workflow permission can bypass templateReferencing: Strict to get host network access, switch service accounts, override pod security context, add tolerations to schedule on control-plane nodes, or enable SA token mounting. This defeats the stated purpose of the feature. The practical impact depends on what Kubernetes-level controls are in place. Clusters with PodSecurity admission or OPA/Gatekeeper would independently block some of these (like hostNetwork). Clusters that rely on Argo's Strict mode as the primary enforcement layer are fully exposed. This issue has been patched in versions 3.7.14 and 4.0.5.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/argoproj/argo-workflows/v3Go | < 3.7.14 | 3.7.14 |
github.com/argoproj/argo-workflows/v4Go | >= 4.0.0, < 4.0.5 | 4.0.5 |
Affected products
1- Range: >= 4.0.0, < 4.0.5
Patches
22727f3f70167Merge commit from fork
4 files changed · +426 −66
workflow/controller/operator.go+19 −8 modified@@ -4283,12 +4283,13 @@ func (woc *wfOperationCtx) retryStrategy(tmpl *wfv1.Template) *wfv1.RetryStrateg func (woc *wfOperationCtx) setExecWorkflow(ctx context.Context) error { if woc.wf.Spec.WorkflowTemplateRef != nil { // not-woc-misuse // When workflow restrictions require template referencing (Strict/Secure mode), - // reject workflows that include a podSpecPatch as it could override security - // settings defined in the WorkflowTemplate. - if woc.controller.Config.WorkflowRestrictions.MustUseReference() && woc.wf.Spec.HasPodSpecPatch() { // not-woc-misuse: intentionally checking the user-submitted spec - err := fmt.Errorf("podSpecPatch is not permitted when using workflowTemplateRef with templateReferencing restriction") - woc.markWorkflowError(ctx, err) - return err + // reject workflows that set any non-allowed fields, as they could override + // security settings defined in the WorkflowTemplate. + if woc.controller.Config.WorkflowRestrictions.MustUseReference() { // not-woc-misuse: intentionally checking the user-submitted spec + if err := wfutil.ValidateUserOverrides(&woc.wf.Spec); err != nil { // not-woc-misuse + woc.markWorkflowError(ctx, err) + return err + } } err := woc.setStoredWfSpec(ctx) if err != nil { @@ -4410,8 +4411,14 @@ func (woc *wfOperationCtx) setStoredWfSpec(ctx context.Context) error { } // Update the Entrypoint, ShutdownStrategy and Suspend if woc.needsStoredWfSpecUpdate() { + // In reference mode, sanitize the user spec before merging so that + // only allow-listed fields participate in the strategic merge patch. + userSpec := &woc.wf.Spec // not-woc-misuse + if woc.controller.Config.WorkflowRestrictions.MustUseReference() { + userSpec = wfutil.SanitizeUserWorkflowSpec(&woc.wf.Spec) // not-woc-misuse + } // Join workflow, workflow template, and workflow default metadata to workflow spec. - mergedWf, err := wfutil.JoinWorkflowSpec(&woc.wf.Spec, workflowTemplateSpec, &wfDefault.Spec) // not-woc-misuse + mergedWf, err := wfutil.JoinWorkflowSpec(userSpec, workflowTemplateSpec, &wfDefault.Spec) if err != nil { return err } @@ -4422,7 +4429,11 @@ func (woc *wfOperationCtx) setStoredWfSpec(ctx context.Context) error { if err != nil { return err } - mergedWf, err := wfutil.JoinWorkflowSpec(&woc.wf.Spec, wftHolder.GetWorkflowSpec(), &wfDefault.Spec) // not-woc-misuse + userSpec := &woc.wf.Spec // not-woc-misuse + if woc.controller.Config.WorkflowRestrictions.MustUseReference() { + userSpec = wfutil.SanitizeUserWorkflowSpec(&woc.wf.Spec) // not-woc-misuse + } + mergedWf, err := wfutil.JoinWorkflowSpec(userSpec, wftHolder.GetWorkflowSpec(), &wfDefault.Spec) if err != nil { return err }
workflow/controller/operator_test.go+105 −4 modified@@ -5665,7 +5665,7 @@ func TestValidReferenceMode(t *testing.T) { assert.Equal(t, wfv1.WorkflowError, woc.wf.Status.Phase) } -func TestReferenceModeBlocksPodSpecPatch(t *testing.T) { +func TestReferenceModeBlocksDisallowedFields(t *testing.T) { wf := wfv1.MustUnmarshalWorkflow("@testdata/workflow-template-ref.yaml") wfTmpl := wfv1.MustUnmarshalWorkflowTemplate("@testdata/workflow-template-submittable.yaml") @@ -5682,7 +5682,8 @@ func TestReferenceModeBlocksPodSpecPatch(t *testing.T) { woc := newWorkflowOperationCtx(ctx, wfWithPatch, controller) woc.operate(ctx) assert.Equal(t, wfv1.WorkflowError, woc.wf.Status.Phase) - assert.Contains(t, woc.wf.Status.Message, "podSpecPatch is not permitted") + assert.Contains(t, woc.wf.Status.Message, "PodSpecPatch") + assert.Contains(t, woc.wf.Status.Message, "not permitted") }) t.Run("Secure rejects podSpecPatch", func(t *testing.T) { @@ -5698,7 +5699,89 @@ func TestReferenceModeBlocksPodSpecPatch(t *testing.T) { woc := newWorkflowOperationCtx(ctx, wfWithPatch, controller) woc.operate(ctx) assert.Equal(t, wfv1.WorkflowError, woc.wf.Status.Phase) - assert.Contains(t, woc.wf.Status.Message, "podSpecPatch is not permitted") + assert.Contains(t, woc.wf.Status.Message, "PodSpecPatch") + assert.Contains(t, woc.wf.Status.Message, "not permitted") + }) + + t.Run("Strict rejects ServiceAccountName", func(t *testing.T) { + wfCopy := wf.DeepCopy() + wfCopy.Spec.ServiceAccountName = "admin" + cancel, controller := newController(logging.TestContext(t.Context()), wfCopy, wfTmpl) + defer cancel() + + ctx := logging.TestContext(t.Context()) + controller.Config.WorkflowRestrictions = &config.WorkflowRestrictions{ + TemplateReferencing: config.TemplateReferencingStrict, + } + woc := newWorkflowOperationCtx(ctx, wfCopy, controller) + woc.operate(ctx) + assert.Equal(t, wfv1.WorkflowError, woc.wf.Status.Phase) + assert.Contains(t, woc.wf.Status.Message, "ServiceAccountName") + }) + + t.Run("Strict rejects SecurityContext", func(t *testing.T) { + wfCopy := wf.DeepCopy() + wfCopy.Spec.SecurityContext = &apiv1.PodSecurityContext{} + cancel, controller := newController(logging.TestContext(t.Context()), wfCopy, wfTmpl) + defer cancel() + + ctx := logging.TestContext(t.Context()) + controller.Config.WorkflowRestrictions = &config.WorkflowRestrictions{ + TemplateReferencing: config.TemplateReferencingStrict, + } + woc := newWorkflowOperationCtx(ctx, wfCopy, controller) + woc.operate(ctx) + assert.Equal(t, wfv1.WorkflowError, woc.wf.Status.Phase) + assert.Contains(t, woc.wf.Status.Message, "SecurityContext") + }) + + t.Run("Strict rejects Templates", func(t *testing.T) { + wfCopy := wf.DeepCopy() + wfCopy.Spec.Templates = []wfv1.Template{{Name: "injected"}} + cancel, controller := newController(logging.TestContext(t.Context()), wfCopy, wfTmpl) + defer cancel() + + ctx := logging.TestContext(t.Context()) + controller.Config.WorkflowRestrictions = &config.WorkflowRestrictions{ + TemplateReferencing: config.TemplateReferencingStrict, + } + woc := newWorkflowOperationCtx(ctx, wfCopy, controller) + woc.operate(ctx) + assert.Equal(t, wfv1.WorkflowError, woc.wf.Status.Phase) + assert.Contains(t, woc.wf.Status.Message, "Templates") + }) + + t.Run("Strict rejects Volumes", func(t *testing.T) { + wfCopy := wf.DeepCopy() + wfCopy.Spec.Volumes = []apiv1.Volume{{Name: "secret-vol"}} + cancel, controller := newController(logging.TestContext(t.Context()), wfCopy, wfTmpl) + defer cancel() + + ctx := logging.TestContext(t.Context()) + controller.Config.WorkflowRestrictions = &config.WorkflowRestrictions{ + TemplateReferencing: config.TemplateReferencingStrict, + } + woc := newWorkflowOperationCtx(ctx, wfCopy, controller) + woc.operate(ctx) + assert.Equal(t, wfv1.WorkflowError, woc.wf.Status.Phase) + assert.Contains(t, woc.wf.Status.Message, "Volumes") + }) + + t.Run("Strict rejects HostNetwork", func(t *testing.T) { + wfCopy := wf.DeepCopy() + hostNet := true + wfCopy.Spec.HostNetwork = &hostNet + cancel, controller := newController(logging.TestContext(t.Context()), wfCopy, wfTmpl) + defer cancel() + + ctx := logging.TestContext(t.Context()) + controller.Config.WorkflowRestrictions = &config.WorkflowRestrictions{ + TemplateReferencing: config.TemplateReferencingStrict, + } + woc := newWorkflowOperationCtx(ctx, wfCopy, controller) + woc.operate(ctx) + assert.Equal(t, wfv1.WorkflowError, woc.wf.Status.Phase) + assert.Contains(t, woc.wf.Status.Message, "HostNetwork") }) t.Run("No restrictions allows podSpecPatch", func(t *testing.T) { @@ -5714,7 +5797,25 @@ func TestReferenceModeBlocksPodSpecPatch(t *testing.T) { assert.Equal(t, wfv1.WorkflowRunning, woc.wf.Status.Phase) }) - t.Run("Without podSpecPatch still works in Strict mode", func(t *testing.T) { + t.Run("Allowed fields pass in Strict mode", func(t *testing.T) { + wfCopy := wf.DeepCopy() + // Set allowed fields only - entrypoint must match one defined in the template + wfCopy.Spec.Entrypoint = "whalesay-template" + wfCopy.Spec.Shutdown = wfv1.ShutdownStrategyTerminate + cancel, controller := newController(logging.TestContext(t.Context()), wfCopy, wfTmpl) + defer cancel() + + ctx := logging.TestContext(t.Context()) + controller.Config.WorkflowRestrictions = &config.WorkflowRestrictions{ + TemplateReferencing: config.TemplateReferencingStrict, + } + woc := newWorkflowOperationCtx(ctx, wfCopy, controller) + woc.operate(ctx) + // Shutdown=Terminate means it won't reach Running, but it shouldn't be Error + assert.NotEqual(t, wfv1.WorkflowError, woc.wf.Status.Phase) + }) + + t.Run("Without disallowed fields still works in Strict mode", func(t *testing.T) { cancel, controller := newController(logging.TestContext(t.Context()), wf, wfTmpl) defer cancel()
workflow/util/merge.go+70 −0 modified@@ -2,13 +2,83 @@ package util import ( "encoding/json" + "fmt" + "reflect" + "sort" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/strategicpatch" wfv1 "github.com/argoproj/argo-workflows/v4/pkg/apis/workflow/v1alpha1" ) +// allowedUserOverrideFields lists WorkflowSpec fields that users may set when +// submitting a workflow via workflowTemplateRef under Strict/Secure mode. +// Any field NOT in this set is blocked by default, ensuring new fields added +// to WorkflowSpec are denied until explicitly reviewed. +var allowedUserOverrideFields = map[string]bool{ + "Arguments": true, + "Entrypoint": true, + "Shutdown": true, + "Suspend": true, + "ActiveDeadlineSeconds": true, + "Priority": true, + "TTLStrategy": true, + "PodGC": true, + "VolumeClaimGC": true, + "ArchiveLogs": true, + "WorkflowMetadata": true, + "WorkflowTemplateRef": true, + "Metrics": true, + "ArtifactGC": true, +} + +// ValidateUserOverrides checks that a user-submitted WorkflowSpec only sets +// fields from the allow-list. Returns an error listing all violations. +func ValidateUserOverrides(userSpec *wfv1.WorkflowSpec) error { + if userSpec == nil { + return nil + } + v := reflect.ValueOf(userSpec).Elem() + t := v.Type() + zero := reflect.New(t).Elem() + + var violations []string + for i := 0; i < t.NumField(); i++ { + fieldName := t.Field(i).Name + if allowedUserOverrideFields[fieldName] { + continue + } + if !reflect.DeepEqual(v.Field(i).Interface(), zero.Field(i).Interface()) { + violations = append(violations, fieldName) + } + } + if len(violations) > 0 { + sort.Strings(violations) + return fmt.Errorf("fields %v are not permitted when using workflowTemplateRef with templateReferencing restriction", violations) + } + return nil +} + +// SanitizeUserWorkflowSpec returns a copy of userSpec with only allow-listed +// fields preserved. This provides defense-in-depth after validation. +func SanitizeUserWorkflowSpec(userSpec *wfv1.WorkflowSpec) *wfv1.WorkflowSpec { + if userSpec == nil { + return nil + } + sanitized := &wfv1.WorkflowSpec{} + src := reflect.ValueOf(userSpec).Elem() + dst := reflect.ValueOf(sanitized).Elem() + t := src.Type() + + for i := 0; i < t.NumField(); i++ { + if allowedUserOverrideFields[t.Field(i).Name] { + dst.Field(i).Set(src.Field(i)) + } + } + return sanitized +} + // MergeTo will merge one workflow (the "patch" workflow) into another (the "target" workflow. // If the target workflow defines a field, this take precedence over the patch. func MergeTo(patch, target *wfv1.Workflow) error {
workflow/util/merge_test.go+232 −54 modified@@ -1,11 +1,14 @@ package util import ( + "reflect" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + apiv1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" wfv1 "github.com/argoproj/argo-workflows/v4/pkg/apis/workflow/v1alpha1" ) @@ -79,40 +82,40 @@ func TestMergeMetaDataTo(t *testing.T) { } var wfDefault = ` -metadata: - annotations: +metadata: + annotations: testAnnotation: default - labels: + labels: testLabel: default -spec: +spec: entrypoint: whalesay activeDeadlineSeconds: 7200 - arguments: - artifacts: + arguments: + artifacts: - name: message path: /tmp/message - parameters: - - + parameters: + - name: message value: "hello world" onExit: whalesay-exit serviceAccountName: default - templates: - - - container: - args: + templates: + - + container: + args: - "hello from the default exit handler" - command: + command: - cowsay image: docker/whalesay name: whalesay-exit - ttlStrategy: + ttlStrategy: secondsAfterCompletion: 60 - volumes: - - + volumes: + - name: test - secret: + secret: secretName: test ` @@ -124,7 +127,7 @@ metadata: namespace: default spec: workflowMetaData: - annotations: + annotations: testAnnotation: wft labels: testLabel: wft @@ -147,16 +150,16 @@ spec: var wf = ` apiVersion: argoproj.io/v1alpha1 kind: Workflow -metadata: +metadata: generateName: hello-world- -spec: +spec: entrypoint: whalesay - templates: - - - container: - args: + templates: + - + container: + args: - "hello world" - command: + command: - cowsay image: "docker/whalesay:latest" name: whalesay @@ -165,62 +168,62 @@ spec: var resultSpec = ` apiVersion: argoproj.io/v1alpha1 kind: Workflow -metadata: +metadata: generateName: hello-world- -spec: +spec: activeDeadlineSeconds: 7200 workflowMetadata: annotations: testAnnotation: wft - labels: - testLabel: wft - arguments: - artifacts: - - + labels: + testLabel: wft + arguments: + artifacts: + - name: message path: /tmp/message - parameters: - - + parameters: + - name: message value: "hello world" entrypoint: whalesay onExit: whalesay-exit serviceAccountName: default - templates: - - - container: - args: + templates: + - + container: + args: - "hello world" - command: + command: - cowsay image: "docker/whalesay:latest" name: whalesay - - - container: - args: + - + container: + args: - "{{inputs.parameters.message}}" - command: + command: - cowsay image: docker/whalesay - inputs: - parameters: - - + inputs: + parameters: + - name: message name: whalesay-template - - - container: - args: + - + container: + args: - "hello from the default exit handler" - command: + command: - cowsay image: docker/whalesay name: whalesay-exit - ttlStrategy: + ttlStrategy: secondsAfterCompletion: 60 - volumes: - - + volumes: + - name: test - secret: + secret: secretName: test ` @@ -549,3 +552,178 @@ func TestMergeLabelsFrom(t *testing.T) { assert.Equal(t, "BASE", targetWf.Spec.WorkflowMetadata.LabelsFrom[`baz`].Expression) }) } + +// blockedUserOverrideFields is used by TestAllWorkflowSpecFieldsAccountedFor +// to verify that every WorkflowSpec field is consciously classified as either +// allowed or blocked. +var blockedUserOverrideFields = map[string]bool{ + "Templates": true, + "TemplateDefaults": true, + "ServiceAccountName": true, + "AutomountServiceAccountToken": true, + "Executor": true, + "Volumes": true, + "VolumeClaimTemplates": true, + "Parallelism": true, + "NodeSelector": true, + "Affinity": true, + "Tolerations": true, + "ImagePullSecrets": true, + "HostNetwork": true, + "DNSPolicy": true, + "DNSConfig": true, + "OnExit": true, + "SchedulerName": true, + "PodPriorityClassName": true, + "HostAliases": true, + "SecurityContext": true, + "PodSpecPatch": true, + "PodDisruptionBudget": true, + "ArtifactRepositoryRef": true, + "Synchronization": true, + "RetryStrategy": true, + "PodMetadata": true, + "Hooks": true, +} + +func TestValidateUserOverrides_AllowedFields(t *testing.T) { + spec := &wfv1.WorkflowSpec{ + Entrypoint: "main", + Arguments: wfv1.Arguments{ + Parameters: []wfv1.Parameter{{Name: "msg", Value: wfv1.AnyStringPtr("hello")}}, + }, + Shutdown: wfv1.ShutdownStrategyTerminate, + Priority: ptr.To[int32](10), + WorkflowTemplateRef: &wfv1.WorkflowTemplateRef{Name: "my-template"}, + } + err := ValidateUserOverrides(spec) + assert.NoError(t, err) +} + +func TestValidateUserOverrides_BlockedFields(t *testing.T) { + tests := []struct { + name string + spec wfv1.WorkflowSpec + field string + }{ + { + name: "ServiceAccountName", + spec: wfv1.WorkflowSpec{ServiceAccountName: "admin"}, + field: "ServiceAccountName", + }, + { + name: "SecurityContext", + spec: wfv1.WorkflowSpec{SecurityContext: &apiv1.PodSecurityContext{}}, + field: "SecurityContext", + }, + { + name: "Templates", + spec: wfv1.WorkflowSpec{Templates: []wfv1.Template{{Name: "evil"}}}, + field: "Templates", + }, + { + name: "Volumes", + spec: wfv1.WorkflowSpec{Volumes: []apiv1.Volume{{Name: "secret-vol"}}}, + field: "Volumes", + }, + { + name: "HostNetwork", + spec: wfv1.WorkflowSpec{HostNetwork: ptr.To(true)}, + field: "HostNetwork", + }, + { + name: "PodSpecPatch", + spec: wfv1.WorkflowSpec{PodSpecPatch: `{"containers":[]}`}, + field: "PodSpecPatch", + }, + { + name: "OnExit", + spec: wfv1.WorkflowSpec{OnExit: "backdoor"}, + field: "OnExit", + }, + { + name: "Hooks", + spec: wfv1.WorkflowSpec{Hooks: wfv1.LifecycleHooks{"exit": wfv1.LifecycleHook{Template: "evil"}}}, + field: "Hooks", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateUserOverrides(&tt.spec) + require.Error(t, err) + assert.Contains(t, err.Error(), tt.field) + assert.Contains(t, err.Error(), "not permitted") + }) + } +} + +func TestValidateUserOverrides_MultipleViolations(t *testing.T) { + spec := &wfv1.WorkflowSpec{ + ServiceAccountName: "admin", + HostNetwork: ptr.To(true), + PodSpecPatch: `{}`, + Templates: []wfv1.Template{{Name: "evil"}}, + } + err := ValidateUserOverrides(spec) + require.Error(t, err) + msg := err.Error() + assert.Contains(t, msg, "ServiceAccountName") + assert.Contains(t, msg, "HostNetwork") + assert.Contains(t, msg, "PodSpecPatch") + assert.Contains(t, msg, "Templates") +} + +func TestValidateUserOverrides_NilSpec(t *testing.T) { + assert.NoError(t, ValidateUserOverrides(nil)) +} + +func TestSanitizeUserWorkflowSpec(t *testing.T) { + spec := &wfv1.WorkflowSpec{ + Entrypoint: "main", + ServiceAccountName: "admin", + HostNetwork: ptr.To(true), + Arguments: wfv1.Arguments{ + Parameters: []wfv1.Parameter{{Name: "msg", Value: wfv1.AnyStringPtr("hello")}}, + }, + Volumes: []apiv1.Volume{{Name: "secret-vol"}}, + Templates: []wfv1.Template{{Name: "evil"}}, + Shutdown: wfv1.ShutdownStrategyTerminate, + WorkflowTemplateRef: &wfv1.WorkflowTemplateRef{Name: "my-template"}, + } + + sanitized := SanitizeUserWorkflowSpec(spec) + + // Allowed fields are preserved + assert.Equal(t, "main", sanitized.Entrypoint) + assert.Equal(t, wfv1.ShutdownStrategyTerminate, sanitized.Shutdown) + assert.Len(t, sanitized.Arguments.Parameters, 1) + assert.Equal(t, "my-template", sanitized.WorkflowTemplateRef.Name) + + // Blocked fields are zeroed + assert.Empty(t, sanitized.ServiceAccountName) + assert.Nil(t, sanitized.HostNetwork) + assert.Nil(t, sanitized.Volumes) + assert.Nil(t, sanitized.Templates) +} + +func TestSanitizeUserWorkflowSpec_Nil(t *testing.T) { + assert.Nil(t, SanitizeUserWorkflowSpec(nil)) +} + +// TestAllWorkflowSpecFieldsAccountedFor is a compile-time safety net. +// It ensures that every field in WorkflowSpec appears in either the +// allowed or blocked list, so new fields force a conscious decision. +func TestAllWorkflowSpecFieldsAccountedFor(t *testing.T) { + specType := reflect.TypeOf(wfv1.WorkflowSpec{}) + for i := 0; i < specType.NumField(); i++ { + fieldName := specType.Field(i).Name + inAllowed := allowedUserOverrideFields[fieldName] + inBlocked := blockedUserOverrideFields[fieldName] + if !inAllowed && !inBlocked { + t.Errorf("WorkflowSpec field %q is not classified in either allowedUserOverrideFields or blockedUserOverrideFields — add it to one of them", fieldName) + } + if inAllowed && inBlocked { + t.Errorf("WorkflowSpec field %q appears in both allowedUserOverrideFields and blockedUserOverrideFields — it should be in exactly one", fieldName) + } + } +}
534f4ff1cbd8Merge commit from fork
2 files changed · +71 −0
workflow/controller/operator.go+8 −0 modified@@ -4303,6 +4303,14 @@ func (woc *wfOperationCtx) retryStrategy(tmpl *wfv1.Template) *wfv1.RetryStrateg func (woc *wfOperationCtx) setExecWorkflow(ctx context.Context) (context.Context, error) { switch { case woc.wf.Spec.WorkflowTemplateRef != nil: // not-woc-misuse + // When workflow restrictions require template referencing (Strict/Secure mode), + // reject workflows that include a podSpecPatch as it could override security + // settings defined in the WorkflowTemplate. + if woc.controller.Config.WorkflowRestrictions.MustUseReference() && woc.wf.Spec.HasPodSpecPatch() { // not-woc-misuse: intentionally checking the user-submitted spec + err := fmt.Errorf("podSpecPatch is not permitted when using workflowTemplateRef with templateReferencing restriction") + ctx = woc.markWorkflowError(ctx, err) + return ctx, err + } err := woc.setStoredWfSpec(ctx) if err != nil { ctx = woc.markWorkflowError(ctx, err)
workflow/controller/operator_test.go+63 −0 modified@@ -5665,6 +5665,69 @@ func TestValidReferenceMode(t *testing.T) { assert.Equal(t, wfv1.WorkflowError, woc.wf.Status.Phase) } +func TestReferenceModeBlocksPodSpecPatch(t *testing.T) { + wf := wfv1.MustUnmarshalWorkflow("@testdata/workflow-template-ref.yaml") + wfTmpl := wfv1.MustUnmarshalWorkflowTemplate("@testdata/workflow-template-submittable.yaml") + + t.Run("Strict rejects podSpecPatch", func(t *testing.T) { + wfWithPatch := wf.DeepCopy() + wfWithPatch.Spec.PodSpecPatch = `{"containers":[{"name":"main","securityContext":{"privileged":true}}]}` + cancel, controller := newController(logging.TestContext(t.Context()), wfWithPatch, wfTmpl) + defer cancel() + + ctx := logging.TestContext(t.Context()) + controller.Config.WorkflowRestrictions = &config.WorkflowRestrictions{ + TemplateReferencing: config.TemplateReferencingStrict, + } + woc := newWorkflowOperationCtx(ctx, wfWithPatch, controller) + woc.operate(ctx) + assert.Equal(t, wfv1.WorkflowError, woc.wf.Status.Phase) + assert.Contains(t, woc.wf.Status.Message, "podSpecPatch is not permitted") + }) + + t.Run("Secure rejects podSpecPatch", func(t *testing.T) { + wfWithPatch := wf.DeepCopy() + wfWithPatch.Spec.PodSpecPatch = `{"containers":[{"name":"main","securityContext":{"runAsUser":0}}]}` + cancel, controller := newController(logging.TestContext(t.Context()), wfWithPatch, wfTmpl) + defer cancel() + + ctx := logging.TestContext(t.Context()) + controller.Config.WorkflowRestrictions = &config.WorkflowRestrictions{ + TemplateReferencing: config.TemplateReferencingSecure, + } + woc := newWorkflowOperationCtx(ctx, wfWithPatch, controller) + woc.operate(ctx) + assert.Equal(t, wfv1.WorkflowError, woc.wf.Status.Phase) + assert.Contains(t, woc.wf.Status.Message, "podSpecPatch is not permitted") + }) + + t.Run("No restrictions allows podSpecPatch", func(t *testing.T) { + wfWithPatch := wf.DeepCopy() + wfWithPatch.Spec.PodSpecPatch = `{"containers":[{"name":"main","resources":{"limits":{"cpu":"1"}}}]}` + cancel, controller := newController(logging.TestContext(t.Context()), wfWithPatch, wfTmpl) + defer cancel() + + ctx := logging.TestContext(t.Context()) + controller.Config.WorkflowRestrictions = nil + woc := newWorkflowOperationCtx(ctx, wfWithPatch, controller) + woc.operate(ctx) + assert.Equal(t, wfv1.WorkflowRunning, woc.wf.Status.Phase) + }) + + t.Run("Without podSpecPatch still works in Strict mode", func(t *testing.T) { + cancel, controller := newController(logging.TestContext(t.Context()), wf, wfTmpl) + defer cancel() + + ctx := logging.TestContext(t.Context()) + controller.Config.WorkflowRestrictions = &config.WorkflowRestrictions{ + TemplateReferencing: config.TemplateReferencingStrict, + } + woc := newWorkflowOperationCtx(ctx, wf, controller) + woc.operate(ctx) + assert.Equal(t, wfv1.WorkflowRunning, woc.wf.Status.Phase) + }) +} + var workflowStatusMetric = ` metadata: name: retry-to-completion-rngcr
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
8- github.com/argoproj/argo-workflows/commit/534f4ff1cbd86908e8ff76d97d553ad5a49a950dnvdPatchWEB
- github.com/argoproj/argo-workflows/security/advisories/GHSA-3775-99mw-8rp4nvdExploitMitigationVendor AdvisoryWEB
- github.com/advisories/GHSA-3775-99mw-8rp4ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-42296ghsaADVISORY
- github.com/argoproj/argo-workflows/commit/2727f3f701677d467dfb5e053c57237cbc752c3cghsaWEB
- github.com/argoproj/argo-workflows/releases/tag/v3.7.14nvdRelease NotesWEB
- github.com/argoproj/argo-workflows/releases/tag/v4.0.5nvdRelease NotesWEB
- github.com/argoproj/argo-workflows/security/advisories/GHSA-3wf5-g532-rcrrghsaWEB
News mentions
0No linked articles in our index yet.