Denial of Service to Argo CD repo-server
Description
Argo CD is a declarative continuous deployment for Kubernetes. All versions of ArgoCD starting from v2.4 have a bug where the ArgoCD repo-server component is vulnerable to a Denial-of-Service attack vector. Specifically, the said component extracts a user-controlled tar.gz file without validating the size of its inner files. As a result, a malicious, low-privileged user can send a malicious tar.gz file that exploits this vulnerability to the repo-server, thereby harming the system's functionality and availability. Additionally, the repo-server is susceptible to another vulnerability due to the fact that it does not check the extracted file permissions before attempting to delete them. Consequently, an attacker can craft a malicious tar.gz archive in a way that prevents the deletion of its inner files when the manifest generation process is completed. A patch for this vulnerability has been released in versions 2.6.15, 2.7.14, and 2.8.3. Users are advised to upgrade. The only way to completely resolve the issue is to upgrade, however users unable to upgrade should configure RBAC (Role-Based Access Control) and provide access for configuring applications only to a limited number of administrators. These administrators should utilize trusted and verified Helm charts.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/argoproj/argo-cd/v2Go | >= 2.4.0, < 2.6.15 | 2.6.15 |
github.com/argoproj/argo-cd/v2Go | >= 2.7.0, < 2.7.14 | 2.7.14 |
github.com/argoproj/argo-cd/v2Go | >= 2.8.0, < 2.8.3 | 2.8.3 |
Affected products
1Patches
21391ba721496Merge pull request from GHSA-fwr2-64vr-xv9m
2 files changed · +40 −0
util/db/cluster.go+5 −0 modified@@ -345,6 +345,9 @@ func clusterToSecret(c *appv1.Cluster, secret *apiv1.Secret) error { secret.Data = data secret.Labels = c.Labels + if c.Annotations != nil && c.Annotations[apiv1.LastAppliedConfigAnnotation] != "" { + return status.Errorf(codes.InvalidArgument, "annotation %s cannot be set", apiv1.LastAppliedConfigAnnotation) + } secret.Annotations = c.Annotations if secret.Annotations == nil { @@ -403,6 +406,8 @@ func SecretToCluster(s *apiv1.Secret) (*appv1.Cluster, error) { annotations := map[string]string{} if s.Annotations != nil { annotations = collections.CopyStringMap(s.Annotations) + // delete system annotations + delete(annotations, apiv1.LastAppliedConfigAnnotation) delete(annotations, common.AnnotationKeyManagedBy) }
util/db/cluster_test.go+35 −0 modified@@ -7,6 +7,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" @@ -56,6 +58,24 @@ func Test_secretToCluster(t *testing.T) { }) } +func Test_secretToCluster_LastAppliedConfigurationDropped(t *testing.T) { + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mycluster", + Namespace: fakeNamespace, + Annotations: map[string]string{v1.LastAppliedConfigAnnotation: "val2"}, + }, + Data: map[string][]byte{ + "name": []byte("test"), + "server": []byte("http://mycluster"), + "config": []byte("{\"username\":\"foo\"}"), + }, + } + cluster, err := SecretToCluster(secret) + require.NoError(t, err) + assert.Len(t, cluster.Annotations, 0) +} + func TestClusterToSecret(t *testing.T) { cluster := &appv1.Cluster{ Server: "server", @@ -78,6 +98,21 @@ func TestClusterToSecret(t *testing.T) { assert.Equal(t, cluster.Labels, s.Labels) } +func TestClusterToSecret_LastAppliedConfigurationRejected(t *testing.T) { + cluster := &appv1.Cluster{ + Server: "server", + Annotations: map[string]string{v1.LastAppliedConfigAnnotation: "val2"}, + Name: "test", + Config: v1alpha1.ClusterConfig{}, + Project: "project", + Namespaces: []string{"default"}, + } + s := &v1.Secret{} + err := clusterToSecret(cluster, s) + require.Error(t, err) + require.Equal(t, codes.InvalidArgument, status.Code(err)) +} + func Test_secretToCluster_NoConfig(t *testing.T) { secret := &v1.Secret{ ObjectMeta: metav1.ObjectMeta{
b8f92c4ff226Merge pull request from GHSA-g687-f2gx-6wm8
12 files changed · +115 −11
cmd/argocd-repo-server/commands/argocd_repo_server.go+8 −0 modified@@ -65,6 +65,8 @@ func NewCommand() *cobra.Command { allowOutOfBoundsSymlinks bool streamedManifestMaxTarSize string streamedManifestMaxExtractedSize string + helmManifestMaxExtractedSize string + disableManifestMaxExtractedSize bool ) var command = cobra.Command{ Use: cliName, @@ -103,6 +105,9 @@ func NewCommand() *cobra.Command { streamedManifestMaxExtractedSizeQuantity, err := resource.ParseQuantity(streamedManifestMaxExtractedSize) errors.CheckError(err) + helmManifestMaxExtractedSizeQuantity, err := resource.ParseQuantity(helmManifestMaxExtractedSize) + errors.CheckError(err) + askPassServer := askpass.NewServer() metricsServer := metrics.NewMetricsServer() cacheutil.CollectMetrics(redisClient, metricsServer) @@ -117,6 +122,7 @@ func NewCommand() *cobra.Command { AllowOutOfBoundsSymlinks: allowOutOfBoundsSymlinks, StreamedManifestMaxExtractedSize: streamedManifestMaxExtractedSizeQuantity.ToDec().Value(), StreamedManifestMaxTarSize: streamedManifestMaxTarSizeQuantity.ToDec().Value(), + HelmManifestMaxExtractedSize: helmManifestMaxExtractedSizeQuantity.ToDec().Value(), }, askPassServer) errors.CheckError(err) @@ -197,6 +203,8 @@ func NewCommand() *cobra.Command { command.Flags().BoolVar(&allowOutOfBoundsSymlinks, "allow-oob-symlinks", env.ParseBoolFromEnv("ARGOCD_REPO_SERVER_ALLOW_OUT_OF_BOUNDS_SYMLINKS", false), "Allow out-of-bounds symlinks in repositories (not recommended)") command.Flags().StringVar(&streamedManifestMaxTarSize, "streamed-manifest-max-tar-size", env.StringFromEnv("ARGOCD_REPO_SERVER_STREAMED_MANIFEST_MAX_TAR_SIZE", "100M"), "Maximum size of streamed manifest archives") command.Flags().StringVar(&streamedManifestMaxExtractedSize, "streamed-manifest-max-extracted-size", env.StringFromEnv("ARGOCD_REPO_SERVER_STREAMED_MANIFEST_MAX_EXTRACTED_SIZE", "1G"), "Maximum size of streamed manifest archives when extracted") + command.Flags().StringVar(&helmManifestMaxExtractedSize, "helm-manifest-max-extracted-size", env.StringFromEnv("ARGOCD_REPO_SERVER_HELM_MANIFEST_MAX_EXTRACTED_SIZE", "1G"), "Maximum size of helm manifest archives when extracted") + command.Flags().BoolVar(&disableManifestMaxExtractedSize, "disable-helm-manifest-max-extracted-size", env.ParseBoolFromEnv("ARGOCD_REPO_SERVER_DISABLE_HELM_MANIFEST_MAX_EXTRACTED_SIZE", false), "Disable maximum size of helm manifest archives when extracted") tlsConfigCustomizerSrc = tls.AddTLSFlagsToCmd(&command) cacheSrc = reposervercache.AddCacheFlagsToCmd(&command, func(client *redis.Client) { redisClient = client
docs/operator-manual/server-commands/argocd-repo-server.md+2 −0 modified@@ -16,7 +16,9 @@ argocd-repo-server [flags] --address string Listen on given address for incoming connections (default "0.0.0.0") --allow-oob-symlinks Allow out-of-bounds symlinks in repositories (not recommended) --default-cache-expiration duration Cache expiration default (default 24h0m0s) + --disable-helm-manifest-max-extracted-size Disable maximum size of helm manifest archives when extracted --disable-tls Disable TLS on the gRPC endpoint + --helm-manifest-max-extracted-size string Maximum size of helm manifest archives when extracted (default "1G") -h, --help help for argocd-repo-server --logformat string Set the logging format. One of: text|json (default "text") --loglevel string Set the logging level. One of: debug|info|warn|error (default "info")
manifests/base/repo-server/argocd-repo-server-deployment.yaml+12 −0 modified@@ -150,6 +150,18 @@ spec: key: reposerver.streamed.manifest.max.extracted.size name: argocd-cmd-params-cm optional: true + - name: ARGOCD_REPO_SERVER_HELM_MANIFEST_MAX_EXTRACTED_SIZE + valueFrom: + configMapKeyRef: + key: reposerver.helm.manifest.max.extracted.size + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_REPO_SERVER_DISABLE_HELM_MANIFEST_MAX_EXTRACTED_SIZE + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: reposerver.disable.helm.manifest.max.extracted.size + optional: true - name: ARGOCD_GIT_MODULES_ENABLED valueFrom: configMapKeyRef:
manifests/core-install.yaml+12 −0 modified@@ -19174,6 +19174,18 @@ spec: key: reposerver.streamed.manifest.max.extracted.size name: argocd-cmd-params-cm optional: true + - name: ARGOCD_REPO_SERVER_HELM_MANIFEST_MAX_EXTRACTED_SIZE + valueFrom: + configMapKeyRef: + key: reposerver.helm.manifest.max.extracted.size + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_REPO_SERVER_DISABLE_HELM_MANIFEST_MAX_EXTRACTED_SIZE + valueFrom: + configMapKeyRef: + key: reposerver.disable.helm.manifest.max.extracted.size + name: argocd-cmd-params-cm + optional: true - name: ARGOCD_GIT_MODULES_ENABLED valueFrom: configMapKeyRef:
manifests/ha/install.yaml+12 −0 modified@@ -20631,6 +20631,18 @@ spec: key: reposerver.streamed.manifest.max.extracted.size name: argocd-cmd-params-cm optional: true + - name: ARGOCD_REPO_SERVER_HELM_MANIFEST_MAX_EXTRACTED_SIZE + valueFrom: + configMapKeyRef: + key: reposerver.helm.manifest.max.extracted.size + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_REPO_SERVER_DISABLE_HELM_MANIFEST_MAX_EXTRACTED_SIZE + valueFrom: + configMapKeyRef: + key: reposerver.disable.helm.manifest.max.extracted.size + name: argocd-cmd-params-cm + optional: true - name: ARGOCD_GIT_MODULES_ENABLED valueFrom: configMapKeyRef:
manifests/ha/namespace-install.yaml+12 −0 modified@@ -2131,6 +2131,18 @@ spec: key: reposerver.streamed.manifest.max.extracted.size name: argocd-cmd-params-cm optional: true + - name: ARGOCD_REPO_SERVER_HELM_MANIFEST_MAX_EXTRACTED_SIZE + valueFrom: + configMapKeyRef: + key: reposerver.helm.manifest.max.extracted.size + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_REPO_SERVER_DISABLE_HELM_MANIFEST_MAX_EXTRACTED_SIZE + valueFrom: + configMapKeyRef: + key: reposerver.disable.helm.manifest.max.extracted.size + name: argocd-cmd-params-cm + optional: true - name: ARGOCD_GIT_MODULES_ENABLED valueFrom: configMapKeyRef:
manifests/install.yaml+12 −0 modified@@ -19688,6 +19688,18 @@ spec: key: reposerver.streamed.manifest.max.extracted.size name: argocd-cmd-params-cm optional: true + - name: ARGOCD_REPO_SERVER_HELM_MANIFEST_MAX_EXTRACTED_SIZE + valueFrom: + configMapKeyRef: + key: reposerver.helm.manifest.max.extracted.size + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_REPO_SERVER_DISABLE_HELM_MANIFEST_MAX_EXTRACTED_SIZE + valueFrom: + configMapKeyRef: + key: reposerver.disable.helm.manifest.max.extracted.size + name: argocd-cmd-params-cm + optional: true - name: ARGOCD_GIT_MODULES_ENABLED valueFrom: configMapKeyRef:
manifests/namespace-install.yaml+12 −0 modified@@ -1188,6 +1188,18 @@ spec: key: reposerver.streamed.manifest.max.extracted.size name: argocd-cmd-params-cm optional: true + - name: ARGOCD_REPO_SERVER_HELM_MANIFEST_MAX_EXTRACTED_SIZE + valueFrom: + configMapKeyRef: + key: reposerver.helm.manifest.max.extracted.size + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_REPO_SERVER_DISABLE_HELM_MANIFEST_MAX_EXTRACTED_SIZE + valueFrom: + configMapKeyRef: + key: reposerver.disable.helm.manifest.max.extracted.size + name: argocd-cmd-params-cm + optional: true - name: ARGOCD_GIT_MODULES_ENABLED valueFrom: configMapKeyRef:
reposerver/repository/repository.go+4 −2 modified@@ -106,6 +106,8 @@ type RepoServerInitConstants struct { AllowOutOfBoundsSymlinks bool StreamedManifestMaxExtractedSize int64 StreamedManifestMaxTarSize int64 + HelmManifestMaxExtractedSize int64 + DisableHelmManifestMaxExtractedSize bool } // NewService returns a new instance of the Manifest service @@ -345,7 +347,7 @@ func (s *Service) runRepoOperation( if source.Helm != nil { helmPassCredentials = source.Helm.PassCredentials } - chartPath, closer, err := helmClient.ExtractChart(source.Chart, revision, helmPassCredentials) + chartPath, closer, err := helmClient.ExtractChart(source.Chart, revision, helmPassCredentials, s.initConstants.HelmManifestMaxExtractedSize, s.initConstants.DisableHelmManifestMaxExtractedSize) if err != nil { return err } @@ -2266,7 +2268,7 @@ func (s *Service) GetRevisionChartDetails(ctx context.Context, q *apiclient.Repo if err != nil { return nil, fmt.Errorf("helm client error: %v", err) } - chartPath, closer, err := helmClient.ExtractChart(q.Name, revision, false) + chartPath, closer, err := helmClient.ExtractChart(q.Name, revision, false, s.initConstants.HelmManifestMaxExtractedSize, s.initConstants.DisableHelmManifestMaxExtractedSize) if err != nil { return nil, fmt.Errorf("error extracting chart: %v", err) }
util/helm/client.go+19 −6 modified@@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "fmt" + executil "github.com/argoproj/argo-cd/v2/util/exec" "io" "net/http" "net/url" @@ -25,7 +26,6 @@ import ( "oras.land/oras-go/v2/registry/remote/auth" "github.com/argoproj/argo-cd/v2/util/cache" - executil "github.com/argoproj/argo-cd/v2/util/exec" argoio "github.com/argoproj/argo-cd/v2/util/io" "github.com/argoproj/argo-cd/v2/util/io/files" "github.com/argoproj/argo-cd/v2/util/proxy" @@ -52,7 +52,7 @@ type indexCache interface { type Client interface { CleanChartCache(chart string, version string) error - ExtractChart(chart string, version string, passCredentials bool) (string, argoio.Closer, error) + ExtractChart(chart string, version string, passCredentials bool, manifestMaxExtractedSize int64, disableManifestMaxExtractedSize bool) (string, argoio.Closer, error) GetIndex(noCache bool) (*Index, error) GetTags(chart string, noCache bool) (*TagsList, error) TestHelmOCI() (bool, error) @@ -122,7 +122,21 @@ func (c *nativeHelmChart) CleanChartCache(chart string, version string) error { return os.RemoveAll(cachePath) } -func (c *nativeHelmChart) ExtractChart(chart string, version string, passCredentials bool) (string, argoio.Closer, error) { +func untarChart(tempDir string, cachedChartPath string, manifestMaxExtractedSize int64, disableManifestMaxExtractedSize bool) error { + if disableManifestMaxExtractedSize { + cmd := exec.Command("tar", "-zxvf", cachedChartPath) + cmd.Dir = tempDir + _, err := executil.Run(cmd) + return err + } + reader, err := os.Open(cachedChartPath) + if err != nil { + return err + } + return files.Untgz(tempDir, reader, manifestMaxExtractedSize, false) +} + +func (c *nativeHelmChart) ExtractChart(chart string, version string, passCredentials bool, manifestMaxExtractedSize int64, disableManifestMaxExtractedSize bool) (string, argoio.Closer, error) { // always use Helm V3 since we don't have chart content to determine correct Helm version helmCmd, err := NewCmdWithVersion("", HelmV3, c.enableOci, c.proxy) @@ -196,15 +210,14 @@ func (c *nativeHelmChart) ExtractChart(chart string, version string, passCredent if len(infos) != 1 { return "", nil, fmt.Errorf("expected 1 file, found %v", len(infos)) } + err = os.Rename(filepath.Join(tempDest, infos[0].Name()), cachedChartPath) if err != nil { return "", nil, err } } - cmd := exec.Command("tar", "-zxvf", cachedChartPath) - cmd.Dir = tempDir - _, err = executil.Run(cmd) + err = untarChart(tempDir, cachedChartPath, manifestMaxExtractedSize, disableManifestMaxExtractedSize) if err != nil { _ = os.RemoveAll(tempDir) return "", nil, err
util/helm/client_test.go+9 −2 modified@@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "math" "os" "strings" "testing" @@ -71,17 +72,23 @@ func TestIndex(t *testing.T) { func Test_nativeHelmChart_ExtractChart(t *testing.T) { client := NewClient("https://argoproj.github.io/argo-helm", Creds{}, false, "") - path, closer, err := client.ExtractChart("argo-cd", "0.7.1", false) + path, closer, err := client.ExtractChart("argo-cd", "0.7.1", false, math.MaxInt64, true) assert.NoError(t, err) defer io.Close(closer) info, err := os.Stat(path) assert.NoError(t, err) assert.True(t, info.IsDir()) } +func Test_nativeHelmChart_ExtractChartWithLimiter(t *testing.T) { + client := NewClient("https://argoproj.github.io/argo-helm", Creds{}, false, "") + _, _, err := client.ExtractChart("argo-cd", "0.7.1", false, 100, false) + assert.Error(t, err, "error while iterating on tar reader: unexpected EOF") +} + func Test_nativeHelmChart_ExtractChart_insecure(t *testing.T) { client := NewClient("https://argoproj.github.io/argo-helm", Creds{InsecureSkipVerify: true}, false, "") - path, closer, err := client.ExtractChart("argo-cd", "0.7.1", false) + path, closer, err := client.ExtractChart("argo-cd", "0.7.1", false, math.MaxInt64, true) assert.NoError(t, err) defer io.Close(closer) info, err := os.Stat(path)
util/helm/mocks/Client.go+1 −1 modified@@ -29,7 +29,7 @@ func (_m *Client) CleanChartCache(chart string, version string) error { } // ExtractChart provides a mock function with given fields: chart, version -func (_m *Client) ExtractChart(chart string, version string, passCredentials bool) (string, io.Closer, error) { +func (_m *Client) ExtractChart(chart string, version string, passCredentials bool, manifestMaxExtractedSize int64, disableManifestMaxExtractedSize bool) (string, io.Closer, error) { ret := _m.Called(chart, version) var r0 string
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/advisories/GHSA-g687-f2gx-6wm8ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-40584ghsaADVISORY
- github.com/argoproj/argo-cd/commit/1391ba72149655e4884d357586d3201f15bc92dcghsaWEB
- github.com/argoproj/argo-cd/commit/b8f92c4ff226346624f43de3f25d81dac6386674ghsax_refsource_MISCWEB
- github.com/argoproj/argo-cd/releases/tag/v2.6.15ghsaWEB
- github.com/argoproj/argo-cd/releases/tag/v2.7.14ghsaWEB
- github.com/argoproj/argo-cd/releases/tag/v2.8.3ghsaWEB
- github.com/argoproj/argo-cd/security/advisories/GHSA-g687-f2gx-6wm8ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.