CVE-2021-37914
Description
In Argo Workflows through 3.1.3, if EXPRESSION_TEMPLATES is enabled and untrusted users are allowed to specify input parameters when running workflows, an attacker may be able to disrupt a workflow because expression template output is evaluated.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
In Argo Workflows (≤3.1.3), enabling EXPRESSION_TEMPLATES lets an attacker supply crafted input parameters to disrupt workflow execution.
Vulnerability
In Argo Workflows through version 3.1.3, the EXPRESSION_TEMPLATES feature evaluates expression template output as literal JSON. When this feature is enabled and untrusted users are allowed to specify input parameters when running workflows, an attacker can inject malicious input that alters the workflow template during parsing [1]. The vulnerability arises because the Go JSON marshaler allows duplicate keys, and the stringified template keys are alphabetically ordered, enabling an attacker to overwrite legitimate fields such as args or env [4].
Exploitation
An attacker requires the ability to submit a workflow with a crafted input parameter. The attacker provides a parameter value that contains JSON fragments designed to overwrite or inject fields into the workflow template. For example, a parameter like "a=\"}\], \"args\": [\"echo nope\"], \"env\": [{\"name\": \"MESSAGE\", \"value\": \"unused\"" can be used to replace the legitimate args field with arbitrary commands [4]. No authentication elevation or network access beyond the ability to submit workflows is needed; the exploitation occurs during template evaluation on the cluster.
Impact
Successful exploitation allows an attacker to rewrite parts of a workflow template at runtime. This can lead to arbitrary command execution in the context of the workflow container (e.g., running echo nope instead of the intended output) [4]. The attacker can alter the intended behavior of workflows, potentially exfiltrating data, modifying results, or disrupting operations. The privilege level achieved is that of the workflow execution environment.
Mitigation
The fix was merged in pull request #6285 and committed in commit 2a2ecc916925642fd8cb1efd026588e6828f82e1 [2][3]. This update ensures that expression templates are properly marshaled and unmarshaled as JSON before evaluation, preventing injection of arbitrary JSON keys. Users should upgrade to Argo Workflows version 3.1.4 or later. If upgrading is not immediately possible, operators should disable the EXPRESSION_TEMPLATES feature or restrict workflow submission privileges to trusted users only. No known KEV listing exists.
- NVD - CVE-2021-37914
- fix(controller): JSON-unmarshal marshaled expression template before … · argoproj/argo-workflows@2a2ecc9
- fix(controller): JSON-unmarshal marshaled expression template before evaluating by crenshaw-dev · Pull Request #6285 · argoproj/argo-workflows
- workflow re-write vulnerability using input parameter
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/argoproj/argo-workflows/v3Go | >= 3.1.0, < 3.1.6 | 3.1.6 |
Affected products
3- Argo/Argo Workflowsdescription
- osv-coords2 versions
< 3.1.4+ 1 more
- (no CPE)range: < 3.1.4
- (no CPE)range: >= 3.1.0, < 3.1.6
Patches
12a2ecc916925fix(controller): JSON-unmarshal marshaled expression template before evaluating (#6285)
8 files changed · +270 −41
docs/fields.md+24 −0 modified@@ -124,6 +124,8 @@ Workflow is the definition of a workflow resource - [`expression-destructure-json.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-destructure-json.yaml) +- [`expression-reusing-verbose-snippets.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-reusing-verbose-snippets.yaml) + - [`expression-tag-template-workflow.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-tag-template-workflow.yaml) - [`fibonacci-seq-conditional-param.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/fibonacci-seq-conditional-param.yaml) @@ -527,6 +529,8 @@ WorkflowSpec is the specification of a Workflow. - [`expression-destructure-json.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-destructure-json.yaml) +- [`expression-reusing-verbose-snippets.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-reusing-verbose-snippets.yaml) + - [`expression-tag-template-workflow.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-tag-template-workflow.yaml) - [`fibonacci-seq-conditional-param.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/fibonacci-seq-conditional-param.yaml) @@ -947,6 +951,8 @@ CronWorkflowSpec is the specification of a CronWorkflow - [`expression-destructure-json.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-destructure-json.yaml) +- [`expression-reusing-verbose-snippets.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-reusing-verbose-snippets.yaml) + - [`expression-tag-template-workflow.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-tag-template-workflow.yaml) - [`fibonacci-seq-conditional-param.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/fibonacci-seq-conditional-param.yaml) @@ -1324,6 +1330,8 @@ WorkflowTemplateSpec is a spec of WorkflowTemplate. - [`expression-destructure-json.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-destructure-json.yaml) +- [`expression-reusing-verbose-snippets.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-reusing-verbose-snippets.yaml) + - [`expression-tag-template-workflow.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-tag-template-workflow.yaml) - [`fibonacci-seq-conditional-param.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/fibonacci-seq-conditional-param.yaml) @@ -1660,6 +1668,8 @@ Arguments to a template - [`expression-destructure-json.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-destructure-json.yaml) +- [`expression-reusing-verbose-snippets.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-reusing-verbose-snippets.yaml) + - [`expression-tag-template-workflow.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-tag-template-workflow.yaml) - [`fibonacci-seq-conditional-param.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/fibonacci-seq-conditional-param.yaml) @@ -2387,6 +2397,8 @@ Parameter indicate a passed string parameter to a service template with an optio - [`expression-destructure-json.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-destructure-json.yaml) +- [`expression-reusing-verbose-snippets.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-reusing-verbose-snippets.yaml) + - [`expression-tag-template-workflow.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-tag-template-workflow.yaml) - [`fibonacci-seq-conditional-param.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/fibonacci-seq-conditional-param.yaml) @@ -2901,6 +2913,8 @@ Inputs are the mechanism for passing parameters, artifacts, volumes from one tem - [`expression-destructure-json.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-destructure-json.yaml) +- [`expression-reusing-verbose-snippets.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-reusing-verbose-snippets.yaml) + - [`expression-tag-template-workflow.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-tag-template-workflow.yaml) - [`fibonacci-seq-conditional-param.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/fibonacci-seq-conditional-param.yaml) @@ -3106,6 +3120,8 @@ ScriptTemplate is a template subtype to enable scripting through code steps - [`expression-destructure-json.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-destructure-json.yaml) +- [`expression-reusing-verbose-snippets.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-reusing-verbose-snippets.yaml) + - [`fibonacci-seq-conditional-param.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/fibonacci-seq-conditional-param.yaml) - [`loops-param-result.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/loops-param-result.yaml) @@ -4077,6 +4093,8 @@ DataSource sources external data into a data template - [`expression-destructure-json.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-destructure-json.yaml) +- [`expression-reusing-verbose-snippets.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-reusing-verbose-snippets.yaml) + - [`fibonacci-seq-conditional-param.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/fibonacci-seq-conditional-param.yaml) - [`loops-param-result.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/loops-param-result.yaml) @@ -4708,6 +4726,8 @@ ObjectMeta is metadata that all persisted resources must have, which includes al - [`expression-destructure-json.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-destructure-json.yaml) +- [`expression-reusing-verbose-snippets.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-reusing-verbose-snippets.yaml) + - [`expression-tag-template-workflow.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-tag-template-workflow.yaml) - [`fibonacci-seq-conditional-param.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/fibonacci-seq-conditional-param.yaml) @@ -5584,6 +5604,8 @@ EnvVar represents an environment variable present in a Container. - [`expression-destructure-json.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-destructure-json.yaml) +- [`expression-reusing-verbose-snippets.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-reusing-verbose-snippets.yaml) + - [`secrets.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/secrets.yaml) - [`sidecar-dind.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/sidecar-dind.yaml) @@ -5989,6 +6011,8 @@ PersistentVolumeClaimSpec describes the common attributes of storage devices and - [`expression-destructure-json.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-destructure-json.yaml) +- [`expression-reusing-verbose-snippets.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-reusing-verbose-snippets.yaml) + - [`expression-tag-template-workflow.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/expression-tag-template-workflow.yaml) - [`fibonacci-seq-conditional-param.yaml`](https://github.com/argoproj/argo-workflows/blob/master/examples/fibonacci-seq-conditional-param.yaml)
examples/expression-reusing-verbose-snippets.yaml+44 −0 added@@ -0,0 +1,44 @@ +# Expressions do not support variables. Rather than repeating verbose expressions to reuse output, you can map over the +# expression output and use its value which is aliased as `#`. Then you can place your "variables" in a JSON object to +# be used elsewhere. +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: expression-reusing-verbose-snippets- +spec: + arguments: + parameters: + - name: weather + # The base64 string is this JSON: {"temps": [34, 27, 15, 57, 46]} + value: '{"weekWeather": "eyJ0ZW1wcyI6IFszNCwgMjcsIDE1LCA1NywgNDZdfQo="}' + entrypoint: main + templates: + - name: main + inputs: + parameters: + - name: week-temps + # The line being mapped over is verbose. Rather than repeat it, we use `map` to alias its output as #. + value: >- + {{= + map([ + jsonpath(sprig.b64dec(jsonpath(workflow.parameters.weather, '$.weekWeather')), '$.temps') + ], { + toJson({ + avg: sprig.add(#[0], #[1], #[2], #[3], #[4]) / 5, + min: sprig.min(#[0], #[1], #[2], #[3], #[4]), + max: sprig.max(#[0], #[1], #[2], #[3], #[4]) + }) + })[0] + }} + script: + env: + - name: AVG + value: "{{=jsonpath(inputs.parameters['week-temps'], '$.avg')}}" + - name: MIN + value: "{{=jsonpath(inputs.parameters['week-temps'], '$.min')}}" + - name: MAX + value: "{{=jsonpath(inputs.parameters['week-temps'], '$.max')}}" + image: debian:9.4 + command: [bash] + source: | + echo "The week's average temperature was $AVG with a minimum of $MIN and a maximum of $MAX."
util/template/expression_template.go+26 −3 modified@@ -1,6 +1,7 @@ package template import ( + "encoding/json" "fmt" "io" "os" @@ -17,12 +18,22 @@ func init() { } func expressionReplace(w io.Writer, expression string, env map[string]interface{}, allowUnresolved bool) (int, error) { - if _, ok := env["retries"]; !ok && hasRetries(expression) && allowUnresolved { + // The template is JSON-marshaled. This JSON-unmarshals the expression to undo any character escapes. + var unmarshalledExpression string + err := json.Unmarshal([]byte(fmt.Sprintf(`"%s"`, expression)), &unmarshalledExpression) + if err != nil && allowUnresolved { + return w.Write([]byte(fmt.Sprintf("{{%s%s}}", kindExpression, expression))) + } + if err != nil { + return 0, fmt.Errorf("failed to unmarshall JSON expression: %w", err) + } + + if _, ok := env["retries"]; !ok && hasRetries(unmarshalledExpression) && allowUnresolved { // this is to make sure expressions like `sprig.int(retries)` don't get resolved to 0 when `retries` don't exist in the env // See https://github.com/argoproj/argo-workflows/issues/5388 return w.Write([]byte(fmt.Sprintf("{{%s%s}}", kindExpression, expression))) } - result, err := expr.Eval(expression, env) + result, err := expr.Eval(unmarshalledExpression, env) if (err != nil || result == nil) && allowUnresolved { // <nil> result is also un-resolved, and any error can be unresolved return w.Write([]byte(fmt.Sprintf("{{%s%s}}", kindExpression, expression))) } @@ -32,7 +43,19 @@ func expressionReplace(w io.Writer, expression string, env map[string]interface{ if result == nil { return 0, fmt.Errorf("failed to evaluate expression %q", expression) } - return w.Write([]byte(fmt.Sprintf("%v", result))) + resultMarshaled, err := json.Marshal(fmt.Sprintf("%v", result)) + if (err != nil || resultMarshaled == nil) && allowUnresolved { + return w.Write([]byte(fmt.Sprintf("{{%s%s}}", kindExpression, expression))) + } + if err != nil { + return 0, fmt.Errorf("failed to marshal evaluated expression: %w", err) + } + if resultMarshaled == nil { + return 0, fmt.Errorf("failed to marshal evaluated marshaled expression %q", expression) + } + // Trim leading and trailing quotes. The value is being inserted into something that's already a string. + marshaledLength := len(resultMarshaled) + return w.Write(resultMarshaled[1 : marshaledLength-1]) } func envMap(replaceMap map[string]string) map[string]interface{} {
util/template/replace.go+21 −1 modified@@ -1,9 +1,29 @@ package template +import ( + "encoding/json" + "errors" +) + +// Replace takes a json-formatted string and performs variable replacement. func Replace(s string, replaceMap map[string]string, allowUnresolved bool) (string, error) { + if !json.Valid([]byte(s)) { + return "", errors.New("cannot do template replacements with invalid JSON") + } + t, err := NewTemplate(s) if err != nil { return "", err } - return t.Replace(replaceMap, allowUnresolved) + + replacedString, err := t.Replace(replaceMap, allowUnresolved) + if err != nil { + return s, err + } + + if !json.Valid([]byte(replacedString)) { + return s, errors.New("cannot finish template replacement because the result was invalid JSON") + } + + return replacedString, nil }
util/template/replace_test.go+38 −32 modified@@ -1,56 +1,62 @@ package template import ( + "encoding/json" "testing" "github.com/stretchr/testify/assert" ) +func toJsonString(v interface{}) string { + jsonString, _ := json.Marshal(v) + return string(jsonString) +} + func Test_Replace(t *testing.T) { - t.Run("InvailedTemplate", func(t *testing.T) { - _, err := Replace("{{", nil, false) + t.Run("InvalidTemplate", func(t *testing.T) { + _, err := Replace(toJsonString("{{"), nil, false) assert.Error(t, err) }) t.Run("Simple", func(t *testing.T) { t.Run("Valid", func(t *testing.T) { - r, err := Replace("{{foo}}", map[string]string{"foo": "bar"}, false) + r, err := Replace(toJsonString("{{foo}}"), map[string]string{"foo": "bar"}, false) assert.NoError(t, err) - assert.Equal(t, "bar", r) + assert.Equal(t, toJsonString("bar"), r) }) t.Run("Unresolved", func(t *testing.T) { t.Run("Allowed", func(t *testing.T) { - _, err := Replace("{{foo}}", nil, true) + _, err := Replace(toJsonString("{{foo}}"), nil, true) assert.NoError(t, err) }) t.Run("Disallowed", func(t *testing.T) { - _, err := Replace("{{foo}}", nil, false) + _, err := Replace(toJsonString("{{foo}}"), nil, false) assert.EqualError(t, err, "failed to resolve {{foo}}") }) }) }) t.Run("Expression", func(t *testing.T) { t.Run("Valid", func(t *testing.T) { - r, err := Replace("{{=foo}}", map[string]string{"foo": "bar"}, false) + r, err := Replace(toJsonString("{{=foo}}"), map[string]string{"foo": "bar"}, false) assert.NoError(t, err) - assert.Equal(t, "bar", r) + assert.Equal(t, toJsonString("bar"), r) }) t.Run("Unresolved", func(t *testing.T) { t.Run("Allowed", func(t *testing.T) { - _, err := Replace("{{=foo}}", nil, true) + _, err := Replace(toJsonString("{{=foo}}"), nil, true) assert.NoError(t, err) }) t.Run("AllowedRetries", func(t *testing.T) { - replaced, err := Replace("{{=sprig.int(retries)}}", nil, true) + replaced, err := Replace(toJsonString("{{=sprig.int(retries)}}"), nil, true) assert.NoError(t, err) - assert.Equal(t, replaced, "{{=sprig.int(retries)}}") + assert.Equal(t, replaced, toJsonString("{{=sprig.int(retries)}}")) }) t.Run("Disallowed", func(t *testing.T) { - _, err := Replace("{{=foo}}", nil, false) + _, err := Replace(toJsonString("{{=foo}}"), nil, false) assert.EqualError(t, err, "failed to evaluate expression \"foo\"") }) }) t.Run("Error", func(t *testing.T) { - _, err := Replace("{{=!}}", nil, false) + _, err := Replace(toJsonString("{{=!}}"), nil, false) if assert.Error(t, err) { assert.Contains(t, err.Error(), "failed to evaluate expression") } @@ -61,53 +67,53 @@ func Test_Replace(t *testing.T) { func TestNestedReplaceString(t *testing.T) { replaceMap := map[string]string{"inputs.parameters.message": "hello world"} - test := `{{- with secret "{{inputs.parameters.message}}" -}} + test := toJsonString(`{{- with secret "{{inputs.parameters.message}}" -}} {{ .Data.data.gitcreds }} - {{- end }}` + {{- end }}`) replacement, err := Replace(test, replaceMap, true) if assert.NoError(t, err) { - assert.Equal(t, "{{- with secret \"hello world\" -}}\n {{ .Data.data.gitcreds }}\n {{- end }}", replacement) + assert.Equal(t, toJsonString("{{- with secret \"hello world\" -}}\n {{ .Data.data.gitcreds }}\n {{- end }}"), replacement) } - test = `{{- with {{ secret "{{inputs.parameters.message}}" -}} + test = toJsonString(`{{- with {{ secret "{{inputs.parameters.message}}" -}} {{ .Data.data.gitcreds }} - {{- end }}` + {{- end }}`) replacement, err = Replace(test, replaceMap, true) if assert.NoError(t, err) { - assert.Equal(t, "{{- with {{ secret \"hello world\" -}}\n {{ .Data.data.gitcreds }}\n {{- end }}", replacement) + assert.Equal(t, toJsonString("{{- with {{ secret \"hello world\" -}}\n {{ .Data.data.gitcreds }}\n {{- end }}"), replacement) } - test = `{{- with {{ secret "{{inputs.parameters.message}}" -}} }} + test = toJsonString(`{{- with {{ secret "{{inputs.parameters.message}}" -}} }} {{ .Data.data.gitcreds }} - {{- end }}` + {{- end }}`) replacement, err = Replace(test, replaceMap, true) if assert.NoError(t, err) { - assert.Equal(t, "{{- with {{ secret \"hello world\" -}} }}\n {{ .Data.data.gitcreds }}\n {{- end }}", replacement) + assert.Equal(t, toJsonString("{{- with {{ secret \"hello world\" -}} }}\n {{ .Data.data.gitcreds }}\n {{- end }}"), replacement) } - test = `{{- with secret "{{inputs.parameters.message}}" -}} }} + test = toJsonString(`{{- with secret "{{inputs.parameters.message}}" -}} }} {{ .Data.data.gitcreds }} - {{- end }}` + {{- end }}`) replacement, err = Replace(test, replaceMap, true) if assert.NoError(t, err) { - assert.Equal(t, "{{- with secret \"hello world\" -}} }}\n {{ .Data.data.gitcreds }}\n {{- end }}", replacement) + assert.Equal(t, toJsonString("{{- with secret \"hello world\" -}} }}\n {{ .Data.data.gitcreds }}\n {{- end }}"), replacement) } - test = `{{- with {{ {{ }} secret "{{inputs.parameters.message}}" -}} }} + test = toJsonString(`{{- with {{ {{ }} secret "{{inputs.parameters.message}}" -}} }} {{ .Data.data.gitcreds }} - {{- end }}` + {{- end }}`) replacement, err = Replace(test, replaceMap, true) if assert.NoError(t, err) { - assert.Equal(t, "{{- with {{ {{ }} secret \"hello world\" -}} }}\n {{ .Data.data.gitcreds }}\n {{- end }}", replacement) + assert.Equal(t, toJsonString("{{- with {{ {{ }} secret \"hello world\" -}} }}\n {{ .Data.data.gitcreds }}\n {{- end }}"), replacement) } - test = `{{- with {{ {{ }} secret "{{does-not-exist}}" -}} }} + test = toJsonString(`{{- with {{ {{ }} secret "{{does-not-exist}}" -}} }} {{ .Data.data.gitcreds }} - {{- end }}` + {{- end }}`) replacement, err = Replace(test, replaceMap, true) if assert.NoError(t, err) { @@ -118,9 +124,9 @@ func TestNestedReplaceString(t *testing.T) { func TestReplaceStringWithWhiteSpace(t *testing.T) { replaceMap := map[string]string{"inputs.parameters.message": "hello world"} - test := `{{ inputs.parameters.message }}` + test := toJsonString(`{{ inputs.parameters.message }}`) replacement, err := Replace(test, replaceMap, true) if assert.NoError(t, err) { - assert.Equal(t, "hello world", replacement) + assert.Equal(t, toJsonString("hello world"), replacement) } }
util/template/template_test.go+83 −0 added@@ -0,0 +1,83 @@ +package template + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +type SimpleValue struct { + Value string `json:"value,omitempty"` +} + +func processTemplate(t *testing.T, tmpl SimpleValue) SimpleValue { + tmplBytes, err := json.Marshal(tmpl) + assert.NoError(t, err) + r, err := Replace(string(tmplBytes), map[string]string{}, true) + assert.NoError(t, err) + var newTmpl SimpleValue + err = json.Unmarshal([]byte(r), &newTmpl) + assert.NoError(t, err) + return newTmpl +} + +func Test_Template_Replace(t *testing.T) { + t.Run("ExpressionWithEscapedCharacters", func(t *testing.T) { + t.Run("SingleQuotes", func(t *testing.T) { + tmpl := SimpleValue{Value: "{{='test'}}"} + newTmpl := processTemplate(t, tmpl) + assert.Equal(t, "test", newTmpl.Value) + }) + t.Run("DoubleQuotes", func(t *testing.T) { + tmpl := SimpleValue{Value: `{{="test"}}`} + newTmpl := processTemplate(t, tmpl) + assert.Equal(t, "test", newTmpl.Value) + }) + t.Run("EscapedBackslashInString", func(t *testing.T) { + tmpl := SimpleValue{Value: `{{='some\\path\\with\\backslashes'}}`} + newTmpl := processTemplate(t, tmpl) + assert.Equal(t, `some\path\with\backslashes`, newTmpl.Value) + }) + t.Run("EscapedNewlineInString", func(t *testing.T) { + tmpl := SimpleValue{Value: `{{='some\nstring\nwith\nescaped\nnewlines'}}`} + newTmpl := processTemplate(t, tmpl) + assert.Equal(t, "some\nstring\nwith\nescaped\nnewlines", newTmpl.Value) + }) + t.Run("Newline", func(t *testing.T) { + tmpl := SimpleValue{Value: "{{=1 + \n1}}"} + newTmpl := processTemplate(t, tmpl) + assert.Equal(t, "2", newTmpl.Value) + }) + t.Run("StringAsJson", func(t *testing.T) { + tmpl := SimpleValue{Value: "{{=toJson('test')}}"} + newTmpl := processTemplate(t, tmpl) + assert.Equal(t, `"test"`, newTmpl.Value) + }) + t.Run("ObjectAsJson", func(t *testing.T) { + tmpl := SimpleValue{Value: "{{=toJson({test: 1})}}"} + newTmpl := processTemplate(t, tmpl) + assert.Equal(t, `{"test":1}`, newTmpl.Value) + }) + t.Run("ArrayAsJson", func(t *testing.T) { + tmpl := SimpleValue{Value: "{{=toJson([1, '2', {an: 'object'}])}}"} + newTmpl := processTemplate(t, tmpl) + assert.Equal(t, `[1,"2",{"an":"object"}]`, newTmpl.Value) + }) + t.Run("SingleQuoteAsString", func(t *testing.T) { + tmpl := SimpleValue{Value: `{{="'"}}`} + newTmpl := processTemplate(t, tmpl) + assert.Equal(t, `'`, newTmpl.Value) + }) + t.Run("DoubleQuoteAsString", func(t *testing.T) { + tmpl := SimpleValue{Value: `{{='"'}}`} + newTmpl := processTemplate(t, tmpl) + assert.Equal(t, `"`, newTmpl.Value) + }) + t.Run("Boolean", func(t *testing.T) { + tmpl := SimpleValue{Value: `{{=true == false}}`} + newTmpl := processTemplate(t, tmpl) + assert.Equal(t, "false", newTmpl.Value) + }) + }) +}
workflow/controller/operator.go+18 −2 modified@@ -3154,12 +3154,28 @@ func (woc *wfOperationCtx) computeMetrics(metricList []*wfv1.Prometheus, localSc metricSpec := metricTmpl.DeepCopy() // Finally substitute value parameters - replacedValue, err := template.Replace(metricSpec.GetValueString(), localScope, false) + metricValueString := metricSpec.GetValueString() + + metricValueStringJson, err := json.Marshal(metricValueString) + if err != nil { + woc.reportMetricEmissionError(fmt.Sprintf("unable to marshal metric to JSON for templating '%s': %s", metricSpec.Name, err)) + continue + } + + replacedValueJson, err := template.Replace(string(metricValueStringJson), localScope, false) if err != nil { woc.reportMetricEmissionError(fmt.Sprintf("unable to substitute parameters for metric '%s': %s", metricSpec.Name, err)) continue } - metricSpec.SetValueString(replacedValue) + + var replacedStringJson string + err = json.Unmarshal([]byte(replacedValueJson), &replacedStringJson) + if err != nil { + woc.reportMetricEmissionError(fmt.Sprintf("unable to unmarshal templated metric JSON '%s': %s", metricSpec.Name, err)) + continue + } + + metricSpec.SetValueString(replacedStringJson) metric := woc.controller.metrics.GetCustomMetric(metricSpec.GetDesc()) // It is valid to pass a nil metric to ConstructOrUpdateMetric, in that case the metric will be created for us
workflow/controller/scope.go+16 −3 modified@@ -1,6 +1,7 @@ package controller import ( + "encoding/json" "fmt" "github.com/antonmedv/expr" @@ -106,13 +107,25 @@ func (s *wfScope) resolveArtifact(art *wfv1.Artifact) (*wfv1.Artifact, error) { } if art.SubPath != "" { - resolvedSubPath, err := template.Replace(art.SubPath, s.getParameters(), true) + // Copy resolved artifact pointer before adding subpath + copyArt := valArt.DeepCopy() + + subPathAsJson, err := json.Marshal(art.SubPath) + if err != nil { + return copyArt, errors.New(errors.CodeBadRequest, "failed to marshal artifact subpath for templating") + } + + resolvedSubPathAsJson, err := template.Replace(string(subPathAsJson), s.getParameters(), true) if err != nil { return nil, err } - // Copy resolved artifact pointer before adding subpath - copyArt := valArt.DeepCopy() + var resolvedSubPath string + err = json.Unmarshal([]byte(resolvedSubPathAsJson), &resolvedSubPath) + if err != nil { + return copyArt, errors.New(errors.CodeBadRequest, "failed to unmarshal artifact subpath for templating") + } + return copyArt, copyArt.AppendToKey(resolvedSubPath) }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8- github.com/advisories/GHSA-h563-xh25-x54qghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2021-37914ghsaADVISORY
- github.com/argoproj/argo-workflows/v3ghsaPACKAGE
- github.com/argoproj/argo-workflows/commit/2a2ecc916925642fd8cb1efd026588e6828f82e1ghsaWEB
- github.com/argoproj/argo-workflows/issues/6441ghsax_refsource_MISCWEB
- github.com/argoproj/argo-workflows/pull/6285ghsaWEB
- github.com/argoproj/argo-workflows/pull/6442ghsax_refsource_MISCWEB
- github.com/argoproj/argo-workflows/security/advisories/GHSA-h563-xh25-x54qghsaWEB
News mentions
0No linked articles in our index yet.