VYPR
Moderate severityNVD Advisory· Published Jun 27, 2022· Updated Apr 23, 2025

Symlink following allows leaking out-of-bounds YAML files from Argo CD repo-server

CVE-2022-31036

Description

Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes. All versions of Argo CD starting with v1.3.0 are vulnerable to a symlink following bug allowing a malicious user with repository write access to leak sensitive YAML files from Argo CD's repo-server. A malicious Argo CD user with write access for a repository which is (or may be) used in a Helm-type Application may commit a symlink which points to an out-of-bounds file. If the target file is a valid YAML file, the attacker can read the contents of that file. Sensitive files which could be leaked include manifest files from other Applications' source repositories (potentially decrypted files, if you are using a decryption plugin) or any YAML-formatted secrets which have been mounted as files on the repo-server. Patches for this vulnerability has been released in the following Argo CD versions: v2.4.1, v2.3.5, v2.2.10 and v2.1.16. If you are using a version >=v2.3.0 and do not have any Helm-type Applications you may disable the Helm config management tool as a workaround.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/argoproj/argo-cdGo
>= 1.3.0, < 2.1.162.1.16
github.com/argoproj/argo-cd/v2Go
< 2.1.162.1.16
github.com/argoproj/argo-cd/v2Go
>= 2.2.0, < 2.2.102.2.10
github.com/argoproj/argo-cd/v2Go
>= 2.3.0, < 2.3.52.3.5
github.com/argoproj/argo-cd/v2Go
>= 2.4.0, < 2.4.12.4.1

Affected products

1

Patches

1
04c305396458

Merge pull request from GHSA-q4w5-4gq2-98vm

https://github.com/argoproj/argo-cdMichael CrenshawJun 21, 2022via ghsa
10 files changed · +63 19
  • reposerver/repository/repository.go+14 9 modified
    @@ -1461,19 +1461,23 @@ func populateHelmAppDetails(res *apiclient.RepoAppDetailsResponse, appPath strin
     		return err
     	}
     
    -	if err := loadFileIntoIfExists(filepath.Join(appPath, "values.yaml"), &res.Helm.Values); err != nil {
    -		return err
    +	if resolvedValuesPath, _, err := pathutil.ResolveFilePath(appPath, repoRoot, "values.yaml", []string{}); err == nil {
    +		if err := loadFileIntoIfExists(resolvedValuesPath, &res.Helm.Values); err != nil {
    +			return err
    +		}
    +	} else {
    +		log.Warnf("Values file %s is not allowed: %v", filepath.Join(appPath, "values.yaml"), err)
     	}
     	var resolvedSelectedValueFiles []pathutil.ResolvedFilePath
     	// drop not allowed values files
     	for _, file := range selectedValueFiles {
     		if resolvedFile, _, err := pathutil.ResolveFilePath(appPath, repoRoot, file, q.GetValuesFileSchemes()); err == nil {
     			resolvedSelectedValueFiles = append(resolvedSelectedValueFiles, resolvedFile)
     		} else {
    -			log.Debugf("Values file %s is not allowed: %v", file, err)
    +			log.Warnf("Values file %s is not allowed: %v", file, err)
     		}
     	}
    -	params, err := h.GetParameters(resolvedSelectedValueFiles)
    +	params, err := h.GetParameters(resolvedSelectedValueFiles, appPath, repoRoot)
     	if err != nil {
     		return err
     	}
    @@ -1492,15 +1496,16 @@ func populateHelmAppDetails(res *apiclient.RepoAppDetailsResponse, appPath strin
     	return nil
     }
     
    -func loadFileIntoIfExists(path string, destination *string) error {
    -	info, err := os.Stat(path)
    +func loadFileIntoIfExists(path pathutil.ResolvedFilePath, destination *string) error {
    +	stringPath := string(path)
    +	info, err := os.Stat(stringPath)
     
     	if err == nil && !info.IsDir() {
    -		if bytes, err := ioutil.ReadFile(path); err != nil {
    -			*destination = string(bytes)
    -		} else {
    +		bytes, err := ioutil.ReadFile(stringPath);
    +		if err != nil {
     			return err
     		}
    +		*destination = string(bytes)
     	}
     
     	return nil
    
  • reposerver/repository/repository_test.go+22 0 modified
    @@ -1125,11 +1125,13 @@ func TestListApps(t *testing.T) {
     		"app-parameters/single-app-only": "Kustomize",
     		"app-parameters/single-global":   "Kustomize",
     		"invalid-helm":                   "Helm",
    +		"in-bounds-values-file-link":     "Helm",
     		"invalid-kustomize":              "Kustomize",
     		"kustomization_yaml":             "Kustomize",
     		"kustomization_yml":              "Kustomize",
     		"my-chart":                       "Helm",
     		"my-chart-2":                     "Helm",
    +		"out-of-bounds-values-file-link": "Helm",
     		"values-files":                   "Helm",
     	}
     	assert.Equal(t, expectedApps, res.Apps)
    @@ -2027,3 +2029,23 @@ func Test_populateHelmAppDetails(t *testing.T) {
     	assert.Len(t, res.Helm.Parameters, 3)
     	assert.Len(t, res.Helm.ValueFiles, 4)
     }
    +
    +func Test_populateHelmAppDetails_values_symlinks(t *testing.T) {
    +	t.Run("inbound", func(t *testing.T) {
    +		res := apiclient.RepoAppDetailsResponse{}
    +		q := apiclient.RepoServerAppDetailsQuery{Repo: &argoappv1.Repository{}, Source: &argoappv1.ApplicationSource{}}
    +		err := populateHelmAppDetails(&res, "./testdata/in-bounds-values-file-link/", "./testdata/in-bounds-values-file-link/", &q)
    +		require.NoError(t, err)
    +		assert.NotEmpty(t, res.Helm.Values)
    +		assert.NotEmpty(t, res.Helm.Parameters)
    +	})
    +
    +	t.Run("out of bounds", func(t *testing.T) {
    +		res := apiclient.RepoAppDetailsResponse{}
    +		q := apiclient.RepoServerAppDetailsQuery{Repo: &argoappv1.Repository{}, Source: &argoappv1.ApplicationSource{}}
    +		err := populateHelmAppDetails(&res, "./testdata/out-of-bounds-values-file-link/", "./testdata/out-of-bounds-values-file-link/", &q)
    +		require.NoError(t, err)
    +		assert.Empty(t, res.Helm.Values)
    +		assert.Empty(t, res.Helm.Parameters)
    +	})
    +}
    
  • reposerver/repository/testdata/in-bounds-values-file-link/Chart.yaml+2 0 added
    @@ -0,0 +1,2 @@
    +name: my-chart
    +version: 1.1.0
    
  • reposerver/repository/testdata/in-bounds-values-file-link/values-2.yaml+1 0 added
    @@ -0,0 +1 @@
    +some: yaml
    
  • reposerver/repository/testdata/in-bounds-values-file-link/values.yaml+1 0 added
    @@ -0,0 +1 @@
    +values-2.yaml
    \ No newline at end of file
    
  • reposerver/repository/testdata/out-of-bounds-values-file-link/Chart.yaml+2 0 added
    @@ -0,0 +1,2 @@
    +name: my-chart
    +version: 1.1.0
    
  • reposerver/repository/testdata/out-of-bounds-values-file-link/values.yaml+1 0 added
    @@ -0,0 +1 @@
    +../out-of-bounds.yaml
    \ No newline at end of file
    
  • reposerver/repository/testdata/out-of-bounds.yaml+1 0 added
    @@ -0,0 +1 @@
    +some: yaml
    
  • util/helm/helm.go+16 7 modified
    @@ -6,9 +6,11 @@ import (
     	"net/url"
     	"os"
     	"os/exec"
    +	"path/filepath"
     	"strings"
     
     	"github.com/ghodss/yaml"
    +	log "github.com/sirupsen/logrus"
     
     	"github.com/argoproj/argo-cd/v2/util/config"
     	executil "github.com/argoproj/argo-cd/v2/util/exec"
    @@ -27,7 +29,7 @@ type Helm interface {
     	// Template returns a list of unstructured objects from a `helm template` command
     	Template(opts *TemplateOpts) (string, error)
     	// GetParameters returns a list of chart parameters taking into account values in provided YAML files.
    -	GetParameters(valuesFiles []pathutil.ResolvedFilePath) (map[string]string, error)
    +	GetParameters(valuesFiles []pathutil.ResolvedFilePath, appPath, repoRoot string) (map[string]string, error)
     	// DependencyBuild runs `helm dependency build` to download a chart's dependencies
     	DependencyBuild() error
     	// Init runs `helm init --client-only`
    @@ -129,12 +131,19 @@ func Version(shortForm bool) (string, error) {
     	return strings.TrimSpace(version), nil
     }
     
    -func (h *helm) GetParameters(valuesFiles []pathutil.ResolvedFilePath) (map[string]string, error) {
    -	out, err := h.cmd.inspectValues(".")
    -	if err != nil {
    -		return nil, err
    +func (h *helm) GetParameters(valuesFiles []pathutil.ResolvedFilePath, appPath, repoRoot string) (map[string]string, error) {
    +	var values []string
    +	// Don't load values.yaml if it's an out-of-bounds link.
    +	if resolved, _, err := pathutil.ResolveFilePath(appPath, repoRoot, "values.yaml", []string{}); err == nil {
    +		fmt.Println(resolved)
    +		out, err := h.cmd.inspectValues(".")
    +		if err != nil {
    +			return nil, err
    +		}
    +		values = append(values, out)
    +	} else {
    +		log.Warnf("Values file %s is not allowed: %v", filepath.Join(appPath, "values.yaml"), err)
     	}
    -	values := []string{out}
     	for i := range valuesFiles {
     		file := string(valuesFiles[i])
     		var fileValues []byte
    @@ -156,7 +165,7 @@ func (h *helm) GetParameters(valuesFiles []pathutil.ResolvedFilePath) (map[strin
     	output := map[string]string{}
     	for _, file := range values {
     		values := map[string]interface{}{}
    -		if err = yaml.Unmarshal([]byte(file), &values); err != nil {
    +		if err := yaml.Unmarshal([]byte(file), &values); err != nil {
     			return nil, fmt.Errorf("failed to parse values: %s", err)
     		}
     		flatVals(values, output)
    
  • util/helm/helm_test.go+3 3 modified
    @@ -85,7 +85,7 @@ func TestHelmGetParams(t *testing.T) {
     	require.NoError(t, err)
     	h, err := NewHelmApp(repoRootAbs, nil, false, "", "", false)
     	assert.NoError(t, err)
    -	params, err := h.GetParameters(nil)
    +	params, err := h.GetParameters(nil, repoRootAbs, repoRootAbs)
     	assert.Nil(t, err)
     
     	slaveCountParam := params["cluster.slaveCount"]
    @@ -100,7 +100,7 @@ func TestHelmGetParamsValueFiles(t *testing.T) {
     	assert.NoError(t, err)
     	valuesPath, _, err := path.ResolveFilePath(repoRootAbs, repoRootAbs, "values-production.yaml", nil)
     	require.NoError(t, err)
    -	params, err := h.GetParameters([]path.ResolvedFilePath{valuesPath})
    +	params, err := h.GetParameters([]path.ResolvedFilePath{valuesPath}, repoRootAbs, repoRootAbs)
     	assert.Nil(t, err)
     
     	slaveCountParam := params["cluster.slaveCount"]
    @@ -117,7 +117,7 @@ func TestHelmGetParamsValueFilesThatExist(t *testing.T) {
     	require.NoError(t, err)
     	valuesProductionPath, _, err := path.ResolveFilePath(repoRootAbs, repoRootAbs, "values-production.yaml", nil)
     	require.NoError(t, err)
    -	params, err := h.GetParameters([]path.ResolvedFilePath{valuesMissingPath, valuesProductionPath})
    +	params, err := h.GetParameters([]path.ResolvedFilePath{valuesMissingPath, valuesProductionPath}, repoRootAbs, repoRootAbs)
     	assert.Nil(t, err)
     
     	slaveCountParam := params["cluster.slaveCount"]
    

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

4

News mentions

0

No linked articles in our index yet.