VYPR
Moderate severityNVD Advisory· Published Jan 30, 2025· Updated Feb 12, 2025

Argo CD does not scrub secret values from patch errors

CVE-2025-23216

Description

Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes. A vulnerability was discovered in Argo CD that exposed secret values in error messages and the diff view when an invalid Kubernetes Secret resource was synced from a repository. The vulnerability assumes the user has write access to the repository and can exploit it, either intentionally or unintentionally, by committing an invalid Secret to repository and triggering a Sync. Once exploited, any user with read access to Argo CD can view the exposed secret data. The vulnerability is fixed in v2.13.4, v2.12.10, and v2.11.13.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/argoproj/argo-cd/v2Go
>= 2.13.0, < 2.13.42.13.4
github.com/argoproj/argo-cd/v2Go
>= 2.12.0, < 2.12.102.12.10
github.com/argoproj/argo-cd/v2Go
< 2.11.132.11.13
github.com/argoproj/argo-cdGo
<= 1.8.7

Affected products

1

Patches

2
6f5537bdf15d

Merge commit from fork

https://github.com/argoproj/argo-cdSiddhesh GhadiJan 29, 2025via ghsa
3 files changed · +56 3
  • go.mod+1 1 modified
    @@ -13,7 +13,7 @@ require (
     	github.com/TomOnTime/utfutil v0.0.0-20180511104225-09c41003ee1d
     	github.com/alicebob/miniredis/v2 v2.34.0
     	github.com/antonmedv/expr v1.15.1
    -	github.com/argoproj/gitops-engine v0.7.1-0.20241216155226-54992bf42431
    +	github.com/argoproj/gitops-engine v0.7.1-0.20250129155113-7e21b91e9d0f
     	github.com/argoproj/notifications-engine v0.4.1-0.20241007194503-2fef5c9049fd
     	github.com/argoproj/pkg v0.13.7-0.20230626144333-d56162821bd1
     	github.com/aws/aws-sdk-go v1.55.6
    
  • go.sum+2 2 modified
    @@ -90,8 +90,8 @@ github.com/antonmedv/expr v1.15.1/go.mod h1:0E/6TxnOlRNp81GMzX9QfDPAmHo2Phg00y4J
     github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
     github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
     github.com/appscode/go v0.0.0-20191119085241-0887d8ec2ecc/go.mod h1:OawnOmAL4ZX3YaPdN+8HTNwBveT1jMsqP74moa9XUbE=
    -github.com/argoproj/gitops-engine v0.7.1-0.20241216155226-54992bf42431 h1:ku0Gzp1dHr7yn83B/xmMrmbB5sJbe32LXaYSDSBd6/c=
    -github.com/argoproj/gitops-engine v0.7.1-0.20241216155226-54992bf42431/go.mod h1:WsnykM8idYRUnneeT31cM/Fq/ZsjkefCbjiD8ioCJkU=
    +github.com/argoproj/gitops-engine v0.7.1-0.20250129155113-7e21b91e9d0f h1:6amQW2gmWyBr/3xz/YzpgrQ+91xKxtpaWiLBkgjjV8o=
    +github.com/argoproj/gitops-engine v0.7.1-0.20250129155113-7e21b91e9d0f/go.mod h1:WsnykM8idYRUnneeT31cM/Fq/ZsjkefCbjiD8ioCJkU=
     github.com/argoproj/notifications-engine v0.4.1-0.20241007194503-2fef5c9049fd h1:lOVVoK89j9Nd4+JYJiKAaMNYC1402C0jICROOfUPWn0=
     github.com/argoproj/notifications-engine v0.4.1-0.20241007194503-2fef5c9049fd/go.mod h1:N0A4sEws2soZjEpY4hgZpQS8mRIEw6otzwfkgc3g9uQ=
     github.com/argoproj/pkg v0.13.7-0.20230626144333-d56162821bd1 h1:qsHwwOJ21K2Ao0xPju1sNuqphyMnMYkyB3ZLoLtxWpo=
    
  • test/e2e/mask_secret_values_test.go+53 0 modified
    @@ -7,6 +7,7 @@ import (
     	"github.com/stretchr/testify/assert"
     
     	"github.com/argoproj/gitops-engine/pkg/health"
    +	"github.com/argoproj/gitops-engine/pkg/sync/common"
     
     	. "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
     	. "github.com/argoproj/argo-cd/v3/test/e2e/fixture"
    @@ -56,3 +57,55 @@ data:
     			assert.False(t, sensitiveData.MatchString(diff))
     		})
     }
    +
    +// Secret values shouldn't be exposed in error messages and the diff view
    +// when invalid secret is synced.
    +func TestMaskValuesInInvalidSecret(t *testing.T) {
    +	sensitiveData := regexp.MustCompile(`SECRETVAL|U0VDUkVUVkFM|12345`)
    +
    +	Given(t).
    +		Path("empty-dir").
    +		When().
    +		// valid secret
    +		AddFile("secrets.yaml", `apiVersion: v1
    +kind: Secret
    +metadata:
    +  name: secret
    +  annotations:
    +    app: test
    +stringData:
    +  username: SECRETVAL
    +data:
    +  password: U0VDUkVUVkFM
    +`).
    +		CreateApp().
    +		Sync().
    +		Then().
    +		Expect(SyncStatusIs(SyncStatusCodeSynced)).
    +		Expect(HealthIs(health.HealthStatusHealthy)).
    +		// secret data shouldn't be exposed in manifests output
    +		And(func(app *Application) {
    +			mnfs, _ := RunCli("app", "manifests", app.Name)
    +			assert.False(t, sensitiveData.MatchString(mnfs))
    +		}).
    +		When().
    +		// invalidate secret
    +		PatchFile("secrets.yaml", `[{"op": "replace", "path": "/data/password", "value": 12345}]`).
    +		Refresh(RefreshTypeHard).
    +		IgnoreErrors().
    +		Sync().
    +		Then().
    +		Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
    +		Expect(OperationPhaseIs(common.OperationFailed)).
    +		// secret data shouldn't be exposed in manifests, diff & error output for invalid secret
    +		And(func(app *Application) {
    +			mnfs, _ := RunCli("app", "manifests", app.Name)
    +			assert.False(t, sensitiveData.MatchString(mnfs))
    +
    +			diff, _ := RunCli("app", "diff", app.Name)
    +			assert.False(t, sensitiveData.MatchString(diff))
    +
    +			msg := app.Status.OperationState.Message
    +			assert.False(t, sensitiveData.MatchString(msg))
    +		})
    +}
    
7e21b91e9d0f

Merge commit from fork

https://github.com/argoproj/gitops-engineSiddhesh GhadiJan 29, 2025via ghsa
4 files changed · +162 9
  • pkg/diff/diff.go+27 9 modified
    @@ -7,6 +7,7 @@ package diff
     import (
     	"bytes"
     	"context"
    +	"encoding/base64"
     	"encoding/json"
     	"errors"
     	"fmt"
    @@ -843,6 +844,32 @@ func NormalizeSecret(un *unstructured.Unstructured, opts ...Option) {
     	if gvk.Group != "" || gvk.Kind != "Secret" {
     		return
     	}
    +
    +	// move stringData to data section
    +	if stringData, found, err := unstructured.NestedMap(un.Object, "stringData"); found && err == nil {
    +		var data map[string]interface{}
    +		data, found, _ = unstructured.NestedMap(un.Object, "data")
    +		if !found {
    +			data = make(map[string]interface{})
    +		}
    +
    +		// base64 encode string values and add non-string values as is.
    +		// This ensures that the apply fails if the secret is invalid.
    +		for k, v := range stringData {
    +			strVal, ok := v.(string)
    +			if ok {
    +				data[k] = base64.StdEncoding.EncodeToString([]byte(strVal))
    +			} else {
    +				data[k] = v
    +			}
    +		}
    +
    +		err := unstructured.SetNestedField(un.Object, data, "data")
    +		if err == nil {
    +			delete(un.Object, "stringData")
    +		}
    +	}
    +
     	o := applyOptions(opts)
     	var secret corev1.Secret
     	err := runtime.DefaultUnstructuredConverter.FromUnstructured(un.Object, &secret)
    @@ -856,15 +883,6 @@ func NormalizeSecret(un *unstructured.Unstructured, opts ...Option) {
     			secret.Data[k] = []byte("")
     		}
     	}
    -	if len(secret.StringData) > 0 {
    -		if secret.Data == nil {
    -			secret.Data = make(map[string][]byte)
    -		}
    -		for k, v := range secret.StringData {
    -			secret.Data[k] = []byte(v)
    -		}
    -		delete(un.Object, "stringData")
    -	}
     	newObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&secret)
     	if err != nil {
     		o.log.Error(err, "object unable to convert from secret")
    
  • pkg/diff/diff_test.go+120 0 modified
    @@ -1044,6 +1044,126 @@ func TestHideSecretDataDifferentKeysDifferentValues(t *testing.T) {
     	assert.Equal(t, map[string]interface{}{"key2": replacement2, "key3": replacement1}, secretData(live))
     }
     
    +func TestHideStringDataInInvalidSecret(t *testing.T) {
    +	liveUn := &unstructured.Unstructured{
    +		Object: map[string]interface{}{
    +			"apiVersion": "v1",
    +			"kind":       "Secret",
    +			"metadata": map[string]interface{}{
    +				"name": "test-secret",
    +			},
    +			"type": "Opaque",
    +			"data": map[string]interface{}{
    +				"key1": "a2V5MQ==",
    +				"key2": "a2V5MQ==",
    +			},
    +		},
    +	}
    +	targetUn := &unstructured.Unstructured{
    +		Object: map[string]interface{}{
    +			"apiVersion": "v1",
    +			"kind":       "Secret",
    +			"metadata": map[string]interface{}{
    +				"name": "test-secret",
    +			},
    +			"type": "Opaque",
    +			"data": map[string]interface{}{
    +				"key1": "a2V5MQ==",
    +				"key2": "a2V5Mg==",
    +				"key3": false,
    +			},
    +			"stringData": map[string]interface{}{
    +				"key4": "key4",
    +				"key5": 5,
    +			},
    +		},
    +	}
    +
    +	liveUn = remarshal(liveUn, applyOptions(diffOptionsForTest()))
    +	targetUn = remarshal(targetUn, applyOptions(diffOptionsForTest()))
    +
    +	target, live, err := HideSecretData(targetUn, liveUn, nil)
    +	require.NoError(t, err)
    +
    +	assert.Equal(t, map[string]interface{}{"key1": replacement1, "key2": replacement2}, secretData(live))
    +	assert.Equal(t, map[string]interface{}{"key1": replacement1, "key2": replacement1, "key3": replacement1, "key4": replacement1, "key5": replacement1}, secretData(target))
    +}
    +
    +// stringData in secrets should be normalized even if it is invalid
    +func TestNormalizeSecret(t *testing.T) {
    +	var tests = []struct {
    +		testname   string
    +		data       map[string]interface{}
    +		stringData map[string]interface{}
    +	}{
    +		{
    +			testname: "Valid secret",
    +			data: map[string]interface{}{
    +				"key1": "key1",
    +			},
    +			stringData: map[string]interface{}{
    +				"key2": "a2V5Mg==",
    +			},
    +		},
    +		{
    +			testname: "Invalid secret",
    +			data: map[string]interface{}{
    +				"key1": "key1",
    +				"key2": 2,
    +			},
    +			stringData: map[string]interface{}{
    +				"key3": "key3",
    +				"key4": nil,
    +			},
    +		},
    +		{
    +			testname: "Invalid secret with stringData only",
    +			data:     nil,
    +			stringData: map[string]interface{}{
    +				"key3": "key3",
    +				"key4": true,
    +			},
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.testname, func(t *testing.T) {
    +			un := &unstructured.Unstructured{
    +				Object: map[string]interface{}{
    +					"apiVersion": "v1",
    +					"kind":       "Secret",
    +					"metadata": map[string]interface{}{
    +						"name": "test-secret",
    +					},
    +					"type":       "Opaque",
    +					"data":       tt.data,
    +					"stringData": tt.stringData,
    +				},
    +			}
    +			un = remarshal(un, applyOptions(diffOptionsForTest()))
    +
    +			NormalizeSecret(un)
    +
    +			_, found, _ := unstructured.NestedMap(un.Object, "stringData")
    +			assert.False(t, found)
    +
    +			data, found, _ := unstructured.NestedMap(un.Object, "data")
    +			assert.True(t, found)
    +
    +			// check all secret keys are found under data in normalized secret
    +			for _, obj := range []map[string]interface{}{tt.data, tt.stringData} {
    +				if obj == nil {
    +					continue
    +				}
    +				for k := range obj {
    +					_, ok := data[k]
    +					assert.True(t, ok)
    +				}
    +			}
    +		})
    +	}
    +}
    +
     func TestHideSecretAnnotations(t *testing.T) {
     	tests := []struct {
     		name           string
    
  • pkg/utils/kube/kube.go+3 0 modified
    @@ -205,12 +205,15 @@ var (
     	// See ApplyOpts::Run()
     	// cmdutil.AddSourceToErr(fmt.Sprintf("applying patch:\n%s\nto:\n%v\nfor:", patchBytes, info), info.Source, err)
     	kubectlApplyPatchErrOutRegexp = regexp.MustCompile(`(?s)^error when applying patch:.*\nfor: "\S+": `)
    +
    +	kubectlErrOutMapRegexp = regexp.MustCompile(`map\[.*\]`)
     )
     
     // cleanKubectlOutput makes the error output of kubectl a little better to read
     func cleanKubectlOutput(s string) string {
     	s = strings.TrimSpace(s)
     	s = kubectlErrOutRegexp.ReplaceAllString(s, "")
    +	s = kubectlErrOutMapRegexp.ReplaceAllString(s, "")
     	s = kubectlApplyPatchErrOutRegexp.ReplaceAllString(s, "")
     	s = strings.Replace(s, "; if you choose to ignore these errors, turn validation off with --validate=false", "", -1)
     	return s
    
  • pkg/utils/kube/kube_test.go+12 0 modified
    @@ -74,6 +74,18 @@ Object: &{map["apiVersion":"v1" "kind":"Service" "metadata":map["annotations":ma
     for: "/var/folders/_m/991sn1ds7g39lnbhp6wvqp9d_j5476/T/224503547": Service "my-service" is invalid: spec.clusterIP: Invalid value: "10.96.0.44": field is immutable`
     		assert.Equal(t, cleanKubectlOutput(s), `Service "my-service" is invalid: spec.clusterIP: Invalid value: "10.96.0.44": field is immutable`)
     	}
    +	{
    +		s := `error when patching "/var/folders/mj/c96jcs7j2cq7xcjfhqjq3m2w0000gn/T/745145319": "" is invalid: patch: Invalid value: "map[data:map[email:aaaaa password:<nil> username:<nil>] metadata:map[annotations:map[kubectl.kubernetes.io/last-applied-configuration:{\"apiVersion\":\"v1\",\"data\":{\"email\":\"aaaaa\"},\"kind\":\"Secret\",\"metadata\":{\"annotations\":{},\"labels\":{\"app.kubernetes.io/instance\":\"test\"},\"name\":\"my-secret\",\"namespace\":\"default\"},\"stringData\":{\"id\":1,\"password\":0,\"username\":\"username\"},\"type\":\"Opaque\"}\n]] stringData:map[id:1 password:0 username:username]]": error decoding from json: illegal base64 data at input byte 4`
    +		assert.Equal(t, cleanKubectlOutput(s), `error when patching "/var/folders/mj/c96jcs7j2cq7xcjfhqjq3m2w0000gn/T/745145319": "" is invalid: patch: Invalid value: "": error decoding from json: illegal base64 data at input byte 4`)
    +	}
    +	{
    +		s := `error when patching "/var/folders/mj/c96jcs7j2cq7xcjfhqjq3m2w0000gn/T/2250018703": "" is invalid: patch: Invalid value: "map[data:<nil> metadata:map[annotations:map[kubectl.kubernetes.io/last-applied-configuration:{\"apiVersion\":\"v1\",\"kind\":\"Secret\",\"metadata\":{\"annotations\":{},\"labels\":{\"app.kubernetes.io/instance\":\"test\"},\"name\":\"my-secret\",\"namespace\":\"default\"},\"stringData\":{\"id\":1,\"password\":0,\"username\":\"username\"},\"type\":\"Opaque\"}\n]]]": cannot convert int64 to string`
    +		assert.Equal(t, cleanKubectlOutput(s), `error when patching "/var/folders/mj/c96jcs7j2cq7xcjfhqjq3m2w0000gn/T/2250018703": "" is invalid: patch: Invalid value: "": cannot convert int64 to string`)
    +	}
    +	{
    +		s := `Secret in version "v1" cannot be handled as a Secret: json: cannot unmarshal bool into Go struct field Secret.data of type []uint8`
    +		assert.Equal(t, cleanKubectlOutput(s), `Secret in version "v1" cannot be handled as a Secret: json: cannot unmarshal bool into Go struct field Secret.data of type []uint8`)
    +	}
     }
     
     func TestInClusterKubeConfig(t *testing.T) {
    

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

6

News mentions

0

No linked articles in our index yet.