Argo CD: Project API Token Exposes Repository Credentials
Description
Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes. In versions 2.13.0 through 2.13.8, 2.14.0 through 2.14.15, 3.0.0 through 3.0.12 and 3.1.0-rc1 through 3.1.1, API tokens with project-level permissions are able to retrieve sensitive repository credentials (usernames, passwords) through the project details API endpoint, even when the token only has standard application management permissions and no explicit access to secrets. This vulnerability does not only affect project-level permissions. Any token with project get permissions is also vulnerable, including global permissions such as: p, role/user, projects, get, *, allow. This issue is fixed in versions 2.13.9, 2.14.16, 3.0.14 and 3.1.2.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/argoproj/argo-cd/v2Go | >= 2.13.0, < 2.13.9 | 2.13.9 |
github.com/argoproj/argo-cd/v2Go | >= 2.14.0, < 2.14.16 | 2.14.16 |
github.com/argoproj/argo-cd/v3Go | < 3.0.14 | 3.0.14 |
github.com/argoproj/argo-cd/v3Go | >= 3.1.0-rc1, < 3.1.2 | 3.1.2 |
Affected products
1Patches
1e8f86101f537fix(security): repository.GetDetailedProject exposes repo secrets (#24387)
10 files changed · +124 −17
docs/operator-manual/upgrading/2.13-2.14.md+9 −1 modified@@ -11,4 +11,12 @@ Eg, `https://github.com/argoproj/argo-cd/manifests/ha/cluster-install?ref=v2.14. ## Upgraded Helm Version Helm was upgraded to 3.16.2 and the skipSchemaValidation Flag was added to -the [CLI and Application CR](https://argo-cd.readthedocs.io/en/latest/user-guide/helm/#helm-skip-schema-validation). \ No newline at end of file +the [CLI and Application CR](https://argo-cd.readthedocs.io/en/latest/user-guide/helm/#helm-skip-schema-validation). + +## Breaking Changes + +## Sanitized project API response + +Due to security reasons ([GHSA-786q-9hcg-v9ff](https://github.com/argoproj/argo-cd/security/advisories/GHSA-786q-9hcg-v9ff)), +the project API response was sanitized to remove sensitive information. This includes +credentials of project-scoped repositories and clusters.
docs/operator-manual/upgrading/2.14-3.0.md+6 −0 modified@@ -492,3 +492,9 @@ resource.customizations.ignoreDifferences.apiextensions.k8s.io_CustomResourceDef ``` More details for ignored resource updates in the [Diffing customization](../../user-guide/diffing.md) documentation. + +### Sanitized project API response + +Due to security reasons ([GHSA-786q-9hcg-v9ff](https://github.com/argoproj/argo-cd/security/advisories/GHSA-786q-9hcg-v9ff)), +the project API response was sanitized to remove sensitive information. This includes +credentials of project-scoped repositories and clusters. \ No newline at end of file
docs/operator-manual/upgrading/3.0-3.1.md+8 −0 modified@@ -55,3 +55,11 @@ Argo CD v3.1 upgrades the bundled Helm version to 3.18.4. There are no breaking Argo CD v3.1 upgrades the bundled Kustomize version to 5.7.0. There are no breaking changes in Kustomize 5.7 according to the [release notes](https://github.com/kubernetes-sigs/kustomize/releases/tag/kustomize%2Fv5.7.0). + +## Breaking Changes + +## Sanitized project API response + +Due to security reasons ([GHSA-786q-9hcg-v9ff](https://github.com/argoproj/argo-cd/security/advisories/GHSA-786q-9hcg-v9ff)), +the project API response was sanitized to remove sensitive information. This includes +credentials of project-scoped repositories and clusters.
docs/operator-manual/upgrading/3.1-3.2.md+8 −0 modified@@ -52,3 +52,11 @@ If you do not want your CronJob to affect the Application's aggregated Health, y `argocd.argoproj.io/ignore-healthcheck: "true"` on the CronJob resource. The health can also be configured globally using the `resource.customizations.health.batch_CronJob` configuration to change the default behaviour. + +## Breaking Changes + +## Sanitized project API response + +Due to security reasons ([GHSA-786q-9hcg-v9ff](https://github.com/argoproj/argo-cd/security/advisories/GHSA-786q-9hcg-v9ff)), +the project API response was sanitized to remove sensitive information. This includes +credentials of project-scoped repositories and clusters.
pkg/apis/application/v1alpha1/repository_types.go+0 −1 modified@@ -347,7 +347,6 @@ func (repo *Repository) Sanitized() *Repository { Repo: repo.Repo, Type: repo.Type, Name: repo.Name, - Username: repo.Username, Insecure: repo.IsInsecure(), EnableLFS: repo.EnableLFS, EnableOCI: repo.EnableOCI,
pkg/apis/application/v1alpha1/types.go+26 −0 modified@@ -2237,6 +2237,32 @@ type Cluster struct { Annotations map[string]string `json:"annotations,omitempty" protobuf:"bytes,13,opt,name=annotations"` } +func (c *Cluster) Sanitized() *Cluster { + return &Cluster{ + ID: c.ID, + Server: c.Server, + Name: c.Name, + Project: c.Project, + Namespaces: c.Namespaces, + Shard: c.Shard, + Labels: c.Labels, + Annotations: c.Annotations, + ClusterResources: c.ClusterResources, + ConnectionState: c.ConnectionState, + ServerVersion: c.ServerVersion, + Info: c.Info, + RefreshRequestedAt: c.RefreshRequestedAt, + Config: ClusterConfig{ + AWSAuthConfig: c.Config.AWSAuthConfig, + ProxyUrl: c.Config.ProxyUrl, + DisableCompression: c.Config.DisableCompression, + TLSClientConfig: TLSClientConfig{ + Insecure: c.Config.Insecure, + }, + }, + } +} + // Equals returns true if two cluster objects are considered to be equal func (c *Cluster) Equals(other *Cluster) bool { if c.Server != other.Server {
pkg/apis/application/v1alpha1/types_test.go+55 −0 modified@@ -4705,3 +4705,58 @@ func TestSyncWindow_Hash(t *testing.T) { require.Equal(t, hash1, hash2, "windows with same core identity but different metadata should produce same hash") }) } + +func TestSanitized(t *testing.T) { + now := metav1.Now() + cluster := &Cluster{ + ID: "123", + Server: "https://example.com", + Name: "example", + ServerVersion: "v1.0.0", + Namespaces: []string{"default", "kube-system"}, + Project: "default", + Labels: map[string]string{ + "env": "production", + }, + Annotations: map[string]string{ + "annotation-key": "annotation-value", + }, + ConnectionState: ConnectionState{ + Status: ConnectionStatusSuccessful, + Message: "Connection successful", + ModifiedAt: &now, + }, + Config: ClusterConfig{ + Username: "admin", + Password: "password123", + BearerToken: "abc", + TLSClientConfig: TLSClientConfig{ + Insecure: true, + }, + ExecProviderConfig: &ExecProviderConfig{ + Command: "test", + }, + }, + } + + assert.Equal(t, &Cluster{ + ID: "123", + Server: "https://example.com", + Name: "example", + ServerVersion: "v1.0.0", + Namespaces: []string{"default", "kube-system"}, + Project: "default", + Labels: map[string]string{"env": "production"}, + Annotations: map[string]string{"annotation-key": "annotation-value"}, + ConnectionState: ConnectionState{ + Status: ConnectionStatusSuccessful, + Message: "Connection successful", + ModifiedAt: &now, + }, + Config: ClusterConfig{ + TLSClientConfig: TLSClientConfig{ + Insecure: true, + }, + }, + }, cluster.Sanitized()) +}
server/cluster/cluster.go+1 −12 modified@@ -471,19 +471,8 @@ func (s *Server) RotateAuth(ctx context.Context, q *cluster.ClusterQuery) (*clus } func (s *Server) toAPIResponse(clust *appv1.Cluster) *appv1.Cluster { + clust = clust.Sanitized() _ = s.cache.GetClusterInfo(clust.Server, &clust.Info) - - clust.Config.Password = "" - clust.Config.BearerToken = "" - clust.Config.KeyData = nil - if clust.Config.ExecProviderConfig != nil { - // We can't know what the user has put into args or - // env vars on the exec provider that might be sensitive - // (e.g. --private-key=XXX, PASSWORD=XXX) - // Implicitly assumes the command executable name is non-sensitive - clust.Config.ExecProviderConfig.Env = make(map[string]string) - clust.Config.ExecProviderConfig.Args = nil - } // populate deprecated fields for backward compatibility //nolint:staticcheck clust.ServerVersion = clust.Info.ServerVersion
server/project/project.go+10 −2 modified@@ -310,12 +310,20 @@ func (s *Server) GetDetailedProject(ctx context.Context, q *project.ProjectQuery } proj.NormalizeJWTTokens() globalProjects := argo.GetGlobalProjects(proj, listersv1alpha1.NewAppProjectLister(s.projInformer.GetIndexer()), s.settingsMgr) + var apiRepos []*v1alpha1.Repository + for _, repo := range repositories { + apiRepos = append(apiRepos, repo.Normalize().Sanitized()) + } + var apiClusters []*v1alpha1.Cluster + for _, cluster := range clusters { + apiClusters = append(apiClusters, cluster.Sanitized()) + } return &project.DetailedProjectsResponse{ GlobalProjects: globalProjects, Project: proj, - Repositories: repositories, - Clusters: clusters, + Repositories: apiRepos, + Clusters: apiClusters, }, err }
server/repository/repository_test.go+1 −1 modified@@ -313,7 +313,7 @@ func TestRepositoryServer(t *testing.T) { testRepo := &appsv1.Repository{ Repo: url, Type: "git", - Username: "foo", + Username: "", InheritedCreds: true, } db.On("ListRepositories", t.Context()).Return([]*appsv1.Repository{testRepo}, nil)
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- github.com/advisories/GHSA-786q-9hcg-v9ffghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-55190ghsaADVISORY
- github.com/argoproj/argo-cd/commit/e8f86101f5378662ae6151ce5c3a76e9141900e8ghsax_refsource_MISCWEB
- github.com/argoproj/argo-cd/security/advisories/GHSA-786q-9hcg-v9ffghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.