Cross-site Scripting for Argo CD single sign on users
Description
Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes. Argo CD starting with 2.3.0 and prior to 2.3.6 and 2.4.5 is vulnerable to a cross-site scripting (XSS) bug which could allow an attacker to inject arbitrary JavaScript in the /auth/callback page in a victim's browser. This vulnerability only affects Argo CD instances which have single sign on (SSO) enabled. The exploit also assumes the attacker has 1) access to the API server's encryption key, 2) a method to add a cookie to the victim's browser, and 3) the ability to convince the victim to visit a malicious /auth/callback link. The vulnerability is classified as low severity because access to the API server's encryption key already grants a high level of access. Exploiting the XSS would allow the attacker to impersonate the victim, but would not grant any privileges which the attacker could not otherwise gain using the encryption key. A patch for this vulnerability has been released in the following Argo CD versions 2.4.5 and 2.3.6. There is currently no known workaround.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/argoproj/argo-cdGo | >= 2.3.0, < 2.3.6 | 2.3.6 |
github.com/argoproj/argo-cdGo | >= 2.4.0, < 2.4.5 | 2.4.5 |
Affected products
1Patches
28d5119b1e303Merge pull request from GHSA-pmjg-52h9-72qv
2 files changed · +64 −4
util/oidc/oidc.go+13 −3 modified@@ -28,6 +28,8 @@ import ( "github.com/argoproj/argo-cd/v2/util/settings" ) +var InvalidRedirectURLError = fmt.Errorf("invalid return URL") + const ( GrantTypeAuthorizationCode = "authorization_code" GrantTypeImplicit = "implicit" @@ -185,10 +187,18 @@ func (a *ClientApp) verifyAppState(r *http.Request, w http.ResponseWriter, state return "", err } cookieVal := string(val) - returnURL := a.baseHRef + redirectURL := a.baseHRef parts := strings.SplitN(cookieVal, ":", 2) if len(parts) == 2 && parts[1] != "" { - returnURL = parts[1] + if !isValidRedirectURL(parts[1], []string{a.settings.URL}) { + sanitizedUrl := parts[1] + if len(sanitizedUrl) > 100 { + sanitizedUrl = sanitizedUrl[:100] + } + log.Warnf("Failed to verify app state - got invalid redirectURL %q", sanitizedUrl) + return "", fmt.Errorf("failed to verify app state: %w", InvalidRedirectURLError) + } + redirectURL = parts[1] } if parts[0] != state { return "", fmt.Errorf("invalid state in '%s' cookie", common.AuthCookieName) @@ -201,7 +211,7 @@ func (a *ClientApp) verifyAppState(r *http.Request, w http.ResponseWriter, state SameSite: http.SameSiteLaxMode, Secure: a.secureCookie, }) - return returnURL, nil + return redirectURL, nil } // isValidRedirectURL checks whether the given redirectURL matches on of the
util/oidc/oidc_test.go+51 −1 modified@@ -191,7 +191,7 @@ func TestGenerateAppState(t *testing.T) { signature, err := util.MakeSignature(32) require.NoError(t, err) expectedReturnURL := "http://argocd.example.com/" - app, err := NewClientApp(&settings.ArgoCDSettings{ServerSignature: signature}, "", "") + app, err := NewClientApp(&settings.ArgoCDSettings{ServerSignature: signature, URL: expectedReturnURL}, "", "") require.NoError(t, err) generateResponse := httptest.NewRecorder() state, err := app.generateAppState(expectedReturnURL, generateResponse) @@ -219,6 +219,56 @@ func TestGenerateAppState(t *testing.T) { }) } +func TestGenerateAppState_XSS(t *testing.T) { + signature, err := util.MakeSignature(32) + require.NoError(t, err) + app, err := NewClientApp( + &settings.ArgoCDSettings{ + // Only return URLs starting with this base should be allowed. + URL: "https://argocd.example.com", + ServerSignature: signature, + }, + "", "", + ) + require.NoError(t, err) + + t.Run("XSS fails", func(t *testing.T) { + // This attack assumes the attacker has compromised the server's secret key. We use `generateAppState` here for + // convenience, but an attacker with access to the server secret could write their own code to generate the + // malicious cookie. + + expectedReturnURL := "javascript: alert('hi')" + generateResponse := httptest.NewRecorder() + state, err := app.generateAppState(expectedReturnURL, generateResponse) + require.NoError(t, err) + + req := httptest.NewRequest("GET", "/", nil) + for _, cookie := range generateResponse.Result().Cookies() { + req.AddCookie(cookie) + } + + returnURL, err := app.verifyAppState(req, httptest.NewRecorder(), state) + assert.ErrorIs(t, err, InvalidRedirectURLError) + assert.Empty(t, returnURL) + }) + + t.Run("valid return URL succeeds", func(t *testing.T) { + expectedReturnURL := "https://argocd.example.com/some/path" + generateResponse := httptest.NewRecorder() + state, err := app.generateAppState(expectedReturnURL, generateResponse) + require.NoError(t, err) + + req := httptest.NewRequest("GET", "/", nil) + for _, cookie := range generateResponse.Result().Cookies() { + req.AddCookie(cookie) + } + + returnURL, err := app.verifyAppState(req, httptest.NewRecorder(), state) + assert.NoError(t, err, InvalidRedirectURLError) + assert.Equal(t, expectedReturnURL, returnURL) + }) +} + func TestGenerateAppState_NoReturnURL(t *testing.T) { signature, err := util.MakeSignature(32) require.NoError(t, err)
3800a1e49d1dMerge pull request from GHSA-pmjg-52h9-72qv
2 files changed · +64 −4
util/oidc/oidc.go+13 −3 modified@@ -28,6 +28,8 @@ import ( "github.com/argoproj/argo-cd/v2/util/settings" ) +var InvalidRedirectURLError = fmt.Errorf("invalid return URL") + const ( GrantTypeAuthorizationCode = "authorization_code" GrantTypeImplicit = "implicit" @@ -185,10 +187,18 @@ func (a *ClientApp) verifyAppState(r *http.Request, w http.ResponseWriter, state return "", err } cookieVal := string(val) - returnURL := a.baseHRef + redirectURL := a.baseHRef parts := strings.SplitN(cookieVal, ":", 2) if len(parts) == 2 && parts[1] != "" { - returnURL = parts[1] + if !isValidRedirectURL(parts[1], []string{a.settings.URL}) { + sanitizedUrl := parts[1] + if len(sanitizedUrl) > 100 { + sanitizedUrl = sanitizedUrl[:100] + } + log.Warnf("Failed to verify app state - got invalid redirectURL %q", sanitizedUrl) + return "", fmt.Errorf("failed to verify app state: %w", InvalidRedirectURLError) + } + redirectURL = parts[1] } if parts[0] != state { return "", fmt.Errorf("invalid state in '%s' cookie", common.AuthCookieName) @@ -201,7 +211,7 @@ func (a *ClientApp) verifyAppState(r *http.Request, w http.ResponseWriter, state SameSite: http.SameSiteLaxMode, Secure: a.secureCookie, }) - return returnURL, nil + return redirectURL, nil } // isValidRedirectURL checks whether the given redirectURL matches on of the
util/oidc/oidc_test.go+51 −1 modified@@ -191,7 +191,7 @@ func TestGenerateAppState(t *testing.T) { signature, err := util.MakeSignature(32) require.NoError(t, err) expectedReturnURL := "http://argocd.example.com/" - app, err := NewClientApp(&settings.ArgoCDSettings{ServerSignature: signature}, "", "") + app, err := NewClientApp(&settings.ArgoCDSettings{ServerSignature: signature, URL: expectedReturnURL}, "", "") require.NoError(t, err) generateResponse := httptest.NewRecorder() state, err := app.generateAppState(expectedReturnURL, generateResponse) @@ -219,6 +219,56 @@ func TestGenerateAppState(t *testing.T) { }) } +func TestGenerateAppState_XSS(t *testing.T) { + signature, err := util.MakeSignature(32) + require.NoError(t, err) + app, err := NewClientApp( + &settings.ArgoCDSettings{ + // Only return URLs starting with this base should be allowed. + URL: "https://argocd.example.com", + ServerSignature: signature, + }, + "", "", + ) + require.NoError(t, err) + + t.Run("XSS fails", func(t *testing.T) { + // This attack assumes the attacker has compromised the server's secret key. We use `generateAppState` here for + // convenience, but an attacker with access to the server secret could write their own code to generate the + // malicious cookie. + + expectedReturnURL := "javascript: alert('hi')" + generateResponse := httptest.NewRecorder() + state, err := app.generateAppState(expectedReturnURL, generateResponse) + require.NoError(t, err) + + req := httptest.NewRequest("GET", "/", nil) + for _, cookie := range generateResponse.Result().Cookies() { + req.AddCookie(cookie) + } + + returnURL, err := app.verifyAppState(req, httptest.NewRecorder(), state) + assert.ErrorIs(t, err, InvalidRedirectURLError) + assert.Empty(t, returnURL) + }) + + t.Run("valid return URL succeeds", func(t *testing.T) { + expectedReturnURL := "https://argocd.example.com/some/path" + generateResponse := httptest.NewRecorder() + state, err := app.generateAppState(expectedReturnURL, generateResponse) + require.NoError(t, err) + + req := httptest.NewRequest("GET", "/", nil) + for _, cookie := range generateResponse.Result().Cookies() { + req.AddCookie(cookie) + } + + returnURL, err := app.verifyAppState(req, httptest.NewRecorder(), state) + assert.NoError(t, err, InvalidRedirectURLError) + assert.Equal(t, expectedReturnURL, returnURL) + }) +} + func TestGenerateAppState_NoReturnURL(t *testing.T) { signature, err := util.MakeSignature(32) require.NoError(t, err)
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
7- github.com/advisories/GHSA-pmjg-52h9-72qvghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-31102ghsaADVISORY
- github.com/argoproj/argo-cd/commit/3800a1e49d1d5a00a6692fee83396a37a6abe89aghsaWEB
- github.com/argoproj/argo-cd/commit/8d5119b1e3038a2c1d8b651cb242525e9e734c4cghsaWEB
- github.com/argoproj/argo-cd/releases/tag/v2.3.6ghsax_refsource_MISCWEB
- github.com/argoproj/argo-cd/releases/tag/v2.4.5ghsax_refsource_MISCWEB
- github.com/argoproj/argo-cd/security/advisories/GHSA-pmjg-52h9-72qvghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.