From 8d5119b1e3038a2c1d8b651cb242525e9e734c4c Mon Sep 17 00:00:00 2001 From: Michael Crenshaw Date: Tue, 12 Jul 2022 08:45:22 -0400 Subject: [PATCH] Merge pull request from GHSA-pmjg-52h9-72qv Signed-off-by: Michael Crenshaw formatting Signed-off-by: Michael Crenshaw fixes from comments Signed-off-by: Michael Crenshaw fix test Signed-off-by: Michael Crenshaw --- util/oidc/oidc.go | 16 ++++++++++--- util/oidc/oidc_test.go | 52 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/util/oidc/oidc.go b/util/oidc/oidc.go index 20b9a5fef8b8a..030388e0c6365 100644 --- a/util/oidc/oidc.go +++ b/util/oidc/oidc.go @@ -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 diff --git a/util/oidc/oidc_test.go b/util/oidc/oidc_test.go index 5c94a661c4161..ec4ecbaff54bd 100644 --- a/util/oidc/oidc_test.go +++ b/util/oidc/oidc_test.go @@ -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)