mirror of https://github.com/grafana/grafana.git
				
				
				
			apply 445-202506261243 manually
This commit is contained in:
		
							parent
							
								
									66f32f6e08
								
							
						
					
					
						commit
						0d9d392834
					
				|  | @ -7,6 +7,8 @@ import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/url" | 	"net/url" | ||||||
|  | 	"path" | ||||||
|  | 	"regexp" | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"github.com/grafana/grafana/pkg/api/response" | 	"github.com/grafana/grafana/pkg/api/response" | ||||||
|  | @ -39,6 +41,9 @@ var getViewIndex = func() string { | ||||||
| 	return viewIndex | 	return viewIndex | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // Only allow redirects that start with an alphanumerical character, a dash or an underscore.
 | ||||||
|  | var redirectRe = regexp.MustCompile(`^/[a-zA-Z0-9-_].*`) | ||||||
|  | 
 | ||||||
| var ( | var ( | ||||||
| 	errAbsoluteRedirectTo  = errors.New("absolute URLs are not allowed for redirect_to cookie value") | 	errAbsoluteRedirectTo  = errors.New("absolute URLs are not allowed for redirect_to cookie value") | ||||||
| 	errInvalidRedirectTo   = errors.New("invalid redirect_to cookie value") | 	errInvalidRedirectTo   = errors.New("invalid redirect_to cookie value") | ||||||
|  | @ -68,6 +73,15 @@ func (hs *HTTPServer) ValidateRedirectTo(redirectTo string) error { | ||||||
| 		return errForbiddenRedirectTo | 		return errForbiddenRedirectTo | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	cleanPath := path.Clean(to.Path) | ||||||
|  | 	// "." is what path.Clean returns for empty paths
 | ||||||
|  | 	if cleanPath == "." { | ||||||
|  | 		return errForbiddenRedirectTo | ||||||
|  | 	} | ||||||
|  | 	if to.Path != "/" && !redirectRe.MatchString(cleanPath) { | ||||||
|  | 		return errForbiddenRedirectTo | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	// when using a subUrl, the redirect_to should start with the subUrl (which contains the leading slash), otherwise the redirect
 | 	// when using a subUrl, the redirect_to should start with the subUrl (which contains the leading slash), otherwise the redirect
 | ||||||
| 	// will send the user to the wrong location
 | 	// will send the user to the wrong location
 | ||||||
| 	if hs.Cfg.AppSubURL != "" && !strings.HasPrefix(to.Path, hs.Cfg.AppSubURL+"/") { | 	if hs.Cfg.AppSubURL != "" && !strings.HasPrefix(to.Path, hs.Cfg.AppSubURL+"/") { | ||||||
|  |  | ||||||
|  | @ -14,20 +14,16 @@ import ( | ||||||
| 	"github.com/grafana/grafana/pkg/services/authn/authntest" | 	"github.com/grafana/grafana/pkg/services/authn/authntest" | ||||||
| 	"github.com/grafana/grafana/pkg/services/secrets/fakes" | 	"github.com/grafana/grafana/pkg/services/secrets/fakes" | ||||||
| 	"github.com/grafana/grafana/pkg/setting" | 	"github.com/grafana/grafana/pkg/setting" | ||||||
|  | 	"github.com/grafana/grafana/pkg/web/webtest" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func setClientWithoutRedirectFollow(t *testing.T) { | func setClientWithoutRedirectFollow(t *testing.T, s *webtest.Server) { | ||||||
| 	t.Helper() | 	t.Helper() | ||||||
| 	old := http.DefaultClient | 	s.HttpClient = &http.Client{ | ||||||
| 	http.DefaultClient = &http.Client{ |  | ||||||
| 		CheckRedirect: func(req *http.Request, via []*http.Request) error { | 		CheckRedirect: func(req *http.Request, via []*http.Request) error { | ||||||
| 			return http.ErrUseLastResponse | 			return http.ErrUseLastResponse | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| 
 |  | ||||||
| 	t.Cleanup(func() { |  | ||||||
| 		http.DefaultClient = old |  | ||||||
| 	}) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestOAuthLogin_Redirect(t *testing.T) { | func TestOAuthLogin_Redirect(t *testing.T) { | ||||||
|  | @ -79,7 +75,7 @@ func TestOAuthLogin_Redirect(t *testing.T) { | ||||||
| 			}) | 			}) | ||||||
| 
 | 
 | ||||||
| 			// we need to prevent the http.Client from following redirects
 | 			// we need to prevent the http.Client from following redirects
 | ||||||
| 			setClientWithoutRedirectFollow(t) | 			setClientWithoutRedirectFollow(t, server) | ||||||
| 
 | 
 | ||||||
| 			res, err := server.Send(server.NewGetRequest("/login/generic_oauth")) | 			res, err := server.Send(server.NewGetRequest("/login/generic_oauth")) | ||||||
| 			require.NoError(t, err) | 			require.NoError(t, err) | ||||||
|  | @ -155,7 +151,7 @@ func TestOAuthLogin_AuthorizationCode(t *testing.T) { | ||||||
| 			}) | 			}) | ||||||
| 
 | 
 | ||||||
| 			// we need to prevent the http.Client from following redirects
 | 			// we need to prevent the http.Client from following redirects
 | ||||||
| 			setClientWithoutRedirectFollow(t) | 			setClientWithoutRedirectFollow(t, server) | ||||||
| 
 | 
 | ||||||
| 			res, err := server.Send(server.NewGetRequest("/login/generic_oauth?code=code")) | 			res, err := server.Send(server.NewGetRequest("/login/generic_oauth?code=code")) | ||||||
| 			require.NoError(t, err) | 			require.NoError(t, err) | ||||||
|  | @ -199,7 +195,7 @@ func TestOAuthLogin_Error(t *testing.T) { | ||||||
| 		hs.SecretsService = fakes.NewFakeSecretsService() | 		hs.SecretsService = fakes.NewFakeSecretsService() | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	setClientWithoutRedirectFollow(t) | 	setClientWithoutRedirectFollow(t, server) | ||||||
| 
 | 
 | ||||||
| 	res, err := server.Send(server.NewGetRequest("/login/azuread?error=someerror")) | 	res, err := server.Send(server.NewGetRequest("/login/azuread?error=someerror")) | ||||||
| 	require.NoError(t, err) | 	require.NoError(t, err) | ||||||
|  |  | ||||||
|  | @ -4,6 +4,7 @@ import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"net/url" | ||||||
| 	"testing" | 	"testing" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
|  | @ -20,6 +21,7 @@ import ( | ||||||
| 	"github.com/grafana/grafana/pkg/services/user" | 	"github.com/grafana/grafana/pkg/services/user" | ||||||
| 	"github.com/grafana/grafana/pkg/services/user/usertest" | 	"github.com/grafana/grafana/pkg/services/user/usertest" | ||||||
| 	"github.com/grafana/grafana/pkg/setting" | 	"github.com/grafana/grafana/pkg/setting" | ||||||
|  | 	"github.com/grafana/grafana/pkg/web/webtest" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func TestUserTokenAPIEndpoint(t *testing.T) { | func TestUserTokenAPIEndpoint(t *testing.T) { | ||||||
|  | @ -150,6 +152,95 @@ func TestUserTokenAPIEndpoint(t *testing.T) { | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func TestHTTPServer_RotateUserAuthTokenRedirect(t *testing.T) { | ||||||
|  | 	redirectTestCases := []struct { | ||||||
|  | 		name        string | ||||||
|  | 		redirectUrl string | ||||||
|  | 		expectedUrl string | ||||||
|  | 	}{ | ||||||
|  | 		// Valid redirects should be preserved
 | ||||||
|  | 		{"valid root path", "/", "/"}, | ||||||
|  | 		{"valid simple path", "/hello", "/hello"}, | ||||||
|  | 		{"valid single char path", "/a", "/a"}, | ||||||
|  | 		{"valid nested path", "/asd/hello", "/asd/hello"}, | ||||||
|  | 
 | ||||||
|  | 		// Invalid redirects should be converted to root
 | ||||||
|  | 		{"backslash domain", `/\grafana.com`, "/"}, | ||||||
|  | 		{"traversal backslash domain", `/a/../\grafana.com`, "/"}, | ||||||
|  | 		{"double slash", "//grafana", "/"}, | ||||||
|  | 		{"missing initial slash", "missingInitialSlash", "/"}, | ||||||
|  | 		{"parent directory", "/../", "/"}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	sessionTestCases := []struct { | ||||||
|  | 		name                      string | ||||||
|  | 		useSessionStorageRedirect bool | ||||||
|  | 	}{ | ||||||
|  | 		{"when useSessionStorageRedirect is enabled", true}, | ||||||
|  | 		{"when useSessionStorageRedirect is disabled", false}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, sessionCase := range sessionTestCases { | ||||||
|  | 		t.Run(sessionCase.name, func(t *testing.T) { | ||||||
|  | 			for _, redirectCase := range redirectTestCases { | ||||||
|  | 				t.Run(redirectCase.name, func(t *testing.T) { | ||||||
|  | 					server := SetupAPITestServer(t, func(hs *HTTPServer) { | ||||||
|  | 						cfg := setting.NewCfg() | ||||||
|  | 						cfg.LoginCookieName = "grafana_session" | ||||||
|  | 						cfg.LoginMaxLifetime = 10 * time.Hour | ||||||
|  | 						hs.Cfg = cfg | ||||||
|  | 						hs.log = log.New() | ||||||
|  | 						hs.AuthTokenService = &authtest.FakeUserAuthTokenService{ | ||||||
|  | 							RotateTokenProvider: func(ctx context.Context, cmd auth.RotateCommand) (*auth.UserToken, error) { | ||||||
|  | 								return &auth.UserToken{UnhashedToken: "new"}, nil | ||||||
|  | 							}, | ||||||
|  | 						} | ||||||
|  | 					}) | ||||||
|  | 
 | ||||||
|  | 					redirectToQuery := url.QueryEscape(redirectCase.redirectUrl) | ||||||
|  | 					urlString := "/user/auth-tokens/rotate" | ||||||
|  | 
 | ||||||
|  | 					if sessionCase.useSessionStorageRedirect { | ||||||
|  | 						urlString = urlString + "?redirectTo=" + redirectToQuery | ||||||
|  | 					} | ||||||
|  | 
 | ||||||
|  | 					req := server.NewGetRequest(urlString) | ||||||
|  | 					req.AddCookie(&http.Cookie{Name: "grafana_session", Value: "123", Path: "/"}) | ||||||
|  | 
 | ||||||
|  | 					if sessionCase.useSessionStorageRedirect { | ||||||
|  | 						req = webtest.RequestWithWebContext(req, &contextmodel.ReqContext{UseSessionStorageRedirect: true}) | ||||||
|  | 					} else { | ||||||
|  | 						req.AddCookie(&http.Cookie{Name: "redirect_to", Value: redirectToQuery, Path: "/"}) | ||||||
|  | 					} | ||||||
|  | 
 | ||||||
|  | 					var redirectStatusCode int | ||||||
|  | 					var redirectLocation string | ||||||
|  | 
 | ||||||
|  | 					server.HttpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { | ||||||
|  | 						if len(via) > 1 { | ||||||
|  | 							// Stop after first redirect
 | ||||||
|  | 							return http.ErrUseLastResponse | ||||||
|  | 						} | ||||||
|  | 
 | ||||||
|  | 						if req.Response == nil { | ||||||
|  | 							return nil | ||||||
|  | 						} | ||||||
|  | 						redirectStatusCode = req.Response.StatusCode | ||||||
|  | 						redirectLocation = req.Response.Header.Get("Location") | ||||||
|  | 						return nil | ||||||
|  | 					} | ||||||
|  | 					res, err := server.Send(req) | ||||||
|  | 					require.NoError(t, err) | ||||||
|  | 					assert.Equal(t, 302, redirectStatusCode) | ||||||
|  | 					assert.Equal(t, redirectCase.expectedUrl, redirectLocation) | ||||||
|  | 
 | ||||||
|  | 					require.NoError(t, res.Body.Close()) | ||||||
|  | 				}) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func TestHTTPServer_RotateUserAuthToken(t *testing.T) { | func TestHTTPServer_RotateUserAuthToken(t *testing.T) { | ||||||
| 	type testCase struct { | 	type testCase struct { | ||||||
| 		desc                 string | 		desc                 string | ||||||
|  |  | ||||||
|  | @ -3,6 +3,8 @@ package middleware | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"path" | ||||||
|  | 	"regexp" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 
 | 
 | ||||||
| 	"github.com/grafana/grafana/pkg/services/contexthandler" | 	"github.com/grafana/grafana/pkg/services/contexthandler" | ||||||
|  | @ -11,6 +13,9 @@ import ( | ||||||
| 	"github.com/grafana/grafana/pkg/web" | 	"github.com/grafana/grafana/pkg/web" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | // Only allow redirects that start with an alphanumerical character, a dash or an underscore.
 | ||||||
|  | var redirectRe = regexp.MustCompile(`^/?[a-zA-Z0-9-_].*`) | ||||||
|  | 
 | ||||||
| // OrgRedirect changes org and redirects users if the
 | // OrgRedirect changes org and redirects users if the
 | ||||||
| // querystring `orgId` doesn't match the active org.
 | // querystring `orgId` doesn't match the active org.
 | ||||||
| func OrgRedirect(cfg *setting.Cfg, userSvc user.Service) web.Handler { | func OrgRedirect(cfg *setting.Cfg, userSvc user.Service) web.Handler { | ||||||
|  | @ -31,6 +36,11 @@ func OrgRedirect(cfg *setting.Cfg, userSvc user.Service) web.Handler { | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | 		if !validRedirectPath(c.Req.URL.Path) { | ||||||
|  | 			// Do not switch orgs or perform the redirect because the new path is not valid
 | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		if err := userSvc.Update(ctx.Req.Context(), &user.UpdateUserCommand{UserID: ctx.UserID, OrgID: &orgId}); err != nil { | 		if err := userSvc.Update(ctx.Req.Context(), &user.UpdateUserCommand{UserID: ctx.UserID, OrgID: &orgId}); err != nil { | ||||||
| 			if ctx.IsApiRequest() { | 			if ctx.IsApiRequest() { | ||||||
| 				ctx.JsonApiErr(404, "Not found", nil) | 				ctx.JsonApiErr(404, "Not found", nil) | ||||||
|  | @ -54,3 +64,8 @@ func OrgRedirect(cfg *setting.Cfg, userSvc user.Service) web.Handler { | ||||||
| 		c.Redirect(newURL, 302) | 		c.Redirect(newURL, 302) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func validRedirectPath(p string) bool { | ||||||
|  | 	cleanPath := path.Clean(p) | ||||||
|  | 	return cleanPath == "." || cleanPath == "/" || redirectRe.MatchString(cleanPath) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ package middleware | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"net/url" | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
| 	"github.com/stretchr/testify/require" | 	"github.com/stretchr/testify/require" | ||||||
|  | @ -23,6 +24,12 @@ func TestOrgRedirectMiddleware(t *testing.T) { | ||||||
| 			expStatus:   302, | 			expStatus:   302, | ||||||
| 			expLocation: "/?orgId=3", | 			expLocation: "/?orgId=3", | ||||||
| 		}, | 		}, | ||||||
|  | 		{ | ||||||
|  | 			desc:        "when setting a correct org for the user with an empty path", | ||||||
|  | 			input:       "?orgId=3", | ||||||
|  | 			expStatus:   302, | ||||||
|  | 			expLocation: "/?orgId=3", | ||||||
|  | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			desc:        "when setting a correct org for the user with '&kiosk'", | 			desc:        "when setting a correct org for the user with '&kiosk'", | ||||||
| 			input:       "/?orgId=3&kiosk", | 			input:       "/?orgId=3&kiosk", | ||||||
|  | @ -64,6 +71,16 @@ func TestOrgRedirectMiddleware(t *testing.T) { | ||||||
| 		require.Equal(t, 404, sc.resp.Code) | 		require.Equal(t, 404, sc.resp.Code) | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
|  | 	middlewareScenario(t, "when redirecting to an invalid path", func(t *testing.T, sc *scenarioContext) { | ||||||
|  | 		sc.withIdentity(&authn.Identity{}) | ||||||
|  | 
 | ||||||
|  | 		path := url.QueryEscape(`/\example.com`) | ||||||
|  | 		sc.m.Get(url.QueryEscape(path), sc.defaultHandler) | ||||||
|  | 		sc.fakeReq("GET", fmt.Sprintf("%s?orgId=3", path)).exec() | ||||||
|  | 
 | ||||||
|  | 		require.Equal(t, 404, sc.resp.Code) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
| 	middlewareScenario(t, "works correctly when grafana is served under a subpath", func(t *testing.T, sc *scenarioContext) { | 	middlewareScenario(t, "works correctly when grafana is served under a subpath", func(t *testing.T, sc *scenarioContext) { | ||||||
| 		sc.withIdentity(&authn.Identity{}) | 		sc.withIdentity(&authn.Identity{}) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -24,6 +24,7 @@ type Server struct { | ||||||
| 	Mux           *web.Mux | 	Mux           *web.Mux | ||||||
| 	RouteRegister routing.RouteRegister | 	RouteRegister routing.RouteRegister | ||||||
| 	TestServer    *httptest.Server | 	TestServer    *httptest.Server | ||||||
|  | 	HttpClient    *http.Client | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NewServer starts and returns a new server.
 | // NewServer starts and returns a new server.
 | ||||||
|  | @ -50,6 +51,7 @@ func NewServer(t testing.TB, routeRegister routing.RouteRegister) *Server { | ||||||
| 		RouteRegister: routeRegister, | 		RouteRegister: routeRegister, | ||||||
| 		Mux:           m, | 		Mux:           m, | ||||||
| 		TestServer:    testServer, | 		TestServer:    testServer, | ||||||
|  | 		HttpClient:    &http.Client{}, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -81,7 +83,7 @@ func (s *Server) NewRequest(method string, target string, body io.Reader) *http. | ||||||
| 
 | 
 | ||||||
| // Send sends a HTTP request to the test server and returns an HTTP response.
 | // Send sends a HTTP request to the test server and returns an HTTP response.
 | ||||||
| func (s *Server) Send(req *http.Request) (*http.Response, error) { | func (s *Server) Send(req *http.Request) (*http.Response, error) { | ||||||
| 	return http.DefaultClient.Do(req) | 	return s.HttpClient.Do(req) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // SendJSON sets the Content-Type header to application/json and sends
 | // SendJSON sets the Content-Type header to application/json and sends
 | ||||||
|  | @ -144,6 +146,7 @@ func requestContextMiddleware() web.Middleware { | ||||||
| 				c.RequestNonce = ctx.RequestNonce | 				c.RequestNonce = ctx.RequestNonce | ||||||
| 				c.PerfmonTimer = ctx.PerfmonTimer | 				c.PerfmonTimer = ctx.PerfmonTimer | ||||||
| 				c.LookupTokenErr = ctx.LookupTokenErr | 				c.LookupTokenErr = ctx.LookupTokenErr | ||||||
|  | 				c.UseSessionStorageRedirect = ctx.UseSessionStorageRedirect | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			next.ServeHTTP(w, r) | 			next.ServeHTTP(w, r) | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue