Compare commits

..

2 Commits

Author SHA1 Message Date
github-actions[bot] 760315278b Release: 11.3.8+security-01 (#108239)
* Update changelog

* Update version to 11.3.8+security-01

* Update CHANGELOG.md

* baldm0mma/ update to workspace for @grafana/plugin-configs

---------

Co-authored-by: grafana-delivery-bot[bot] <grafana-delivery-bot[bot]@users.noreply.github.com>
Co-authored-by: Jev Forsberg <46619047+baldm0mma@users.noreply.github.com>
Co-authored-by: jev forsberg <jev.forsberg@grafana.com>
2025-07-17 11:57:25 -04:00
Serge Zaitsev bd5b3f533c apply patch 448-202507012244 manually 2025-07-02 15:03:53 +02:00
18 changed files with 174 additions and 42 deletions
+5 -9
View File
@@ -1,17 +1,13 @@
<!-- 11.3.8 START -->
<!-- 11.3.8+security-01 START -->
# 11.3.8 (2025-06-17)
### Features and enhancements
- **Dependencies:** Bump Go to v1.24.4 [#106571](https://github.com/grafana/grafana/pull/106571), [@macabu](https://github.com/macabu)
- **Dependencies:** Bump github.com/openfga/openfga to v1.8.13 to address CVE-2025-48371 [#106120](https://github.com/grafana/grafana/pull/106120), [@macabu](https://github.com/macabu)
# 11.3.8+security-01 (2025-07-17)
### Bug fixes
- **Security:** Fixes CVE-2025-3415
- **Security:** Fixed CVE-2025-6023
- **Security:** Fixed CVE-2025-6197
<!-- 11.3.8 END -->
<!-- 11.3.8+security-01 END -->
<!-- 11.3.7 START -->
# 11.3.7 (2025-05-22)
@@ -1,6 +1,6 @@
{
"name": "@test-plugins/extensions-test-app",
"version": "11.3.9",
"version": "11.3.8",
"private": true,
"scripts": {
"build": "webpack -c ./webpack.config.ts --env production",
@@ -12,7 +12,7 @@
"license": "Apache-2.0",
"devDependencies": {
"@grafana/eslint-config": "7.0.0",
"@grafana/plugin-configs": "11.3.9",
"@grafana/plugin-configs": "workspace:*",
"@types/lodash": "4.17.7",
"@types/node": "20.14.14",
"@types/prismjs": "1.26.4",
+14
View File
@@ -7,6 +7,8 @@ import (
"fmt"
"net/http"
"net/url"
"path"
"regexp"
"strings"
"github.com/grafana/grafana/pkg/api/response"
@@ -39,6 +41,9 @@ var getViewIndex = func() string {
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 (
errAbsoluteRedirectTo = errors.New("absolute URLs are not allowed for 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
}
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
// will send the user to the wrong location
if hs.Cfg.AppSubURL != "" && !strings.HasPrefix(to.Path, hs.Cfg.AppSubURL+"/") {
+6 -10
View File
@@ -14,20 +14,16 @@ import (
"github.com/grafana/grafana/pkg/services/authn/authntest"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
"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()
old := http.DefaultClient
http.DefaultClient = &http.Client{
s.HttpClient = &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
t.Cleanup(func() {
http.DefaultClient = old
})
}
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
setClientWithoutRedirectFollow(t)
setClientWithoutRedirectFollow(t, server)
res, err := server.Send(server.NewGetRequest("/login/generic_oauth"))
require.NoError(t, err)
@@ -155,7 +151,7 @@ func TestOAuthLogin_AuthorizationCode(t *testing.T) {
})
// 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"))
require.NoError(t, err)
@@ -199,7 +195,7 @@ func TestOAuthLogin_Error(t *testing.T) {
hs.SecretsService = fakes.NewFakeSecretsService()
})
setClientWithoutRedirectFollow(t)
setClientWithoutRedirectFollow(t, server)
res, err := server.Send(server.NewGetRequest("/login/azuread?error=someerror"))
require.NoError(t, err)
+91
View File
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/http"
"net/url"
"testing"
"time"
@@ -20,6 +21,7 @@ import (
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/user/usertest"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web/webtest"
)
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) {
type testCase struct {
desc string
+15
View File
@@ -3,6 +3,8 @@ package middleware
import (
"fmt"
"net/http"
"path"
"regexp"
"strconv"
"strings"
@@ -12,6 +14,9 @@ import (
"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
// querystring `orgId` doesn't match the active org.
func OrgRedirect(cfg *setting.Cfg, userSvc user.Service) web.Handler {
@@ -32,6 +37,11 @@ func OrgRedirect(cfg *setting.Cfg, userSvc user.Service) web.Handler {
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 ctx.IsApiRequest() {
ctx.JsonApiErr(404, "Not found", nil)
@@ -55,3 +65,8 @@ func OrgRedirect(cfg *setting.Cfg, userSvc user.Service) web.Handler {
c.Redirect(newURL, 302)
}
}
func validRedirectPath(p string) bool {
cleanPath := path.Clean(p)
return cleanPath == "." || cleanPath == "/" || redirectRe.MatchString(cleanPath)
}
+17
View File
@@ -2,6 +2,7 @@ package middleware
import (
"fmt"
"net/url"
"testing"
"github.com/stretchr/testify/require"
@@ -22,6 +23,12 @@ func TestOrgRedirectMiddleware(t *testing.T) {
expStatus: 302,
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'",
input: "/?orgId=3&kiosk",
@@ -62,4 +69,14 @@ func TestOrgRedirectMiddleware(t *testing.T) {
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)
})
}
+4 -1
View File
@@ -24,6 +24,7 @@ type Server struct {
Mux *web.Mux
RouteRegister routing.RouteRegister
TestServer *httptest.Server
HttpClient *http.Client
}
// NewServer starts and returns a new server.
@@ -50,6 +51,7 @@ func NewServer(t testing.TB, routeRegister routing.RouteRegister) *Server {
RouteRegister: routeRegister,
Mux: m,
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.
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
@@ -144,6 +146,7 @@ func requestContextMiddleware() web.Middleware {
c.RequestNonce = ctx.RequestNonce
c.PerfmonTimer = ctx.PerfmonTimer
c.LookupTokenErr = ctx.LookupTokenErr
c.UseSessionStorageRedirect = ctx.UseSessionStorageRedirect
}
next.ServeHTTP(w, r)
@@ -26,7 +26,7 @@
},
"devDependencies": {
"@grafana/e2e-selectors": "11.3.9",
"@grafana/plugin-configs": "11.3.9",
"@grafana/plugin-configs": "workspace:*",
"@testing-library/dom": "10.0.0",
"@testing-library/react": "15.0.2",
"@testing-library/user-event": "14.5.2",
@@ -27,7 +27,7 @@
},
"devDependencies": {
"@grafana/e2e-selectors": "11.3.9",
"@grafana/plugin-configs": "11.3.9",
"@grafana/plugin-configs": "workspace:*",
"@testing-library/dom": "10.0.0",
"@testing-library/react": "15.0.2",
"@testing-library/user-event": "14.5.2",
@@ -17,7 +17,7 @@
},
"devDependencies": {
"@grafana/e2e-selectors": "11.3.9",
"@grafana/plugin-configs": "11.3.9",
"@grafana/plugin-configs": "workspace:*",
"@testing-library/react": "15.0.2",
"@testing-library/user-event": "14.5.2",
"@types/jest": "29.5.13",
@@ -20,7 +20,7 @@
"tslib": "2.7.0"
},
"devDependencies": {
"@grafana/plugin-configs": "11.3.9",
"@grafana/plugin-configs": "workspace:*",
"@testing-library/dom": "10.0.0",
"@testing-library/jest-dom": "6.4.2",
"@testing-library/react": "15.0.2",
@@ -23,7 +23,7 @@
},
"devDependencies": {
"@grafana/e2e-selectors": "11.3.9",
"@grafana/plugin-configs": "11.3.9",
"@grafana/plugin-configs": "workspace:*",
"@testing-library/dom": "10.0.0",
"@testing-library/react": "15.0.2",
"@testing-library/user-event": "14.5.2",
@@ -17,7 +17,7 @@
},
"devDependencies": {
"@grafana/e2e-selectors": "11.3.9",
"@grafana/plugin-configs": "11.3.9",
"@grafana/plugin-configs": "workspace:*",
"@testing-library/react": "15.0.2",
"@testing-library/user-event": "14.5.2",
"@types/jest": "29.5.13",
@@ -17,7 +17,7 @@
},
"devDependencies": {
"@grafana/e2e-selectors": "11.3.9",
"@grafana/plugin-configs": "11.3.9",
"@grafana/plugin-configs": "workspace:*",
"@testing-library/react": "15.0.2",
"@testing-library/user-event": "14.5.2",
"@types/jest": "29.5.13",
@@ -18,7 +18,7 @@
"tslib": "2.7.0"
},
"devDependencies": {
"@grafana/plugin-configs": "11.3.9",
"@grafana/plugin-configs": "workspace:*",
"@testing-library/dom": "10.0.0",
"@testing-library/react": "15.0.2",
"@testing-library/user-event": "14.5.2",
@@ -39,7 +39,7 @@
"uuid": "9.0.1"
},
"devDependencies": {
"@grafana/plugin-configs": "11.3.9",
"@grafana/plugin-configs": "workspace:*",
"@testing-library/dom": "10.0.0",
"@testing-library/jest-dom": "6.4.2",
"@testing-library/react": "15.0.2",
+11 -11
View File
@@ -3136,7 +3136,7 @@ __metadata:
"@grafana/data": "npm:11.3.9"
"@grafana/e2e-selectors": "npm:11.3.9"
"@grafana/experimental": "npm:2.1.1"
"@grafana/plugin-configs": "npm:11.3.9"
"@grafana/plugin-configs": "workspace:*"
"@grafana/runtime": "npm:11.3.9"
"@grafana/schema": "npm:11.3.9"
"@grafana/ui": "npm:11.3.9"
@@ -3180,7 +3180,7 @@ __metadata:
"@grafana/data": "npm:11.3.9"
"@grafana/e2e-selectors": "npm:11.3.9"
"@grafana/experimental": "npm:2.1.1"
"@grafana/plugin-configs": "npm:11.3.9"
"@grafana/plugin-configs": "workspace:*"
"@grafana/runtime": "npm:11.3.9"
"@grafana/sql": "npm:11.3.9"
"@grafana/ui": "npm:11.3.9"
@@ -3209,7 +3209,7 @@ __metadata:
dependencies:
"@emotion/css": "npm:11.13.4"
"@grafana/data": "npm:11.3.9"
"@grafana/plugin-configs": "npm:11.3.9"
"@grafana/plugin-configs": "workspace:*"
"@grafana/runtime": "npm:11.3.9"
"@grafana/schema": "npm:11.3.9"
"@grafana/ui": "npm:11.3.9"
@@ -3252,7 +3252,7 @@ __metadata:
"@grafana/data": "npm:11.3.9"
"@grafana/e2e-selectors": "npm:11.3.9"
"@grafana/experimental": "npm:2.1.1"
"@grafana/plugin-configs": "npm:11.3.9"
"@grafana/plugin-configs": "workspace:*"
"@grafana/runtime": "npm:11.3.9"
"@grafana/schema": "npm:11.3.9"
"@grafana/ui": "npm:11.3.9"
@@ -3335,7 +3335,7 @@ __metadata:
"@grafana/data": "npm:11.3.9"
"@grafana/e2e-selectors": "npm:11.3.9"
"@grafana/experimental": "npm:2.1.1"
"@grafana/plugin-configs": "npm:11.3.9"
"@grafana/plugin-configs": "workspace:*"
"@grafana/runtime": "npm:11.3.9"
"@grafana/sql": "npm:11.3.9"
"@grafana/ui": "npm:11.3.9"
@@ -3366,7 +3366,7 @@ __metadata:
"@grafana/data": "npm:11.3.9"
"@grafana/e2e-selectors": "npm:11.3.9"
"@grafana/experimental": "npm:2.1.1"
"@grafana/plugin-configs": "npm:11.3.9"
"@grafana/plugin-configs": "workspace:*"
"@grafana/runtime": "npm:11.3.9"
"@grafana/sql": "npm:11.3.9"
"@grafana/ui": "npm:11.3.9"
@@ -3395,7 +3395,7 @@ __metadata:
dependencies:
"@emotion/css": "npm:11.13.4"
"@grafana/data": "npm:11.3.9"
"@grafana/plugin-configs": "npm:11.3.9"
"@grafana/plugin-configs": "workspace:*"
"@grafana/runtime": "npm:11.3.9"
"@grafana/schema": "npm:11.3.9"
"@grafana/ui": "npm:11.3.9"
@@ -3430,7 +3430,7 @@ __metadata:
"@grafana/e2e-selectors": "npm:11.3.9"
"@grafana/experimental": "npm:2.1.1"
"@grafana/google-sdk": "npm:0.1.2"
"@grafana/plugin-configs": "npm:11.3.9"
"@grafana/plugin-configs": "workspace:*"
"@grafana/runtime": "npm:11.3.9"
"@grafana/schema": "npm:11.3.9"
"@grafana/ui": "npm:11.3.9"
@@ -3481,7 +3481,7 @@ __metadata:
"@grafana/lezer-traceql": "npm:0.0.19"
"@grafana/monaco-logql": "npm:^0.0.7"
"@grafana/o11y-ds-frontend": "workspace:*"
"@grafana/plugin-configs": "npm:11.3.9"
"@grafana/plugin-configs": "workspace:*"
"@grafana/runtime": "workspace:*"
"@grafana/schema": "workspace:*"
"@grafana/ui": "workspace:*"
@@ -3885,7 +3885,7 @@ __metadata:
languageName: unknown
linkType: soft
"@grafana/plugin-configs@npm:11.3.9, @grafana/plugin-configs@workspace:*, @grafana/plugin-configs@workspace:packages/grafana-plugin-configs":
"@grafana/plugin-configs@workspace:*, @grafana/plugin-configs@workspace:packages/grafana-plugin-configs":
version: 0.0.0-use.local
resolution: "@grafana/plugin-configs@workspace:packages/grafana-plugin-configs"
dependencies:
@@ -9162,7 +9162,7 @@ __metadata:
"@emotion/css": "npm:11.11.2"
"@grafana/data": "workspace:*"
"@grafana/eslint-config": "npm:7.0.0"
"@grafana/plugin-configs": "npm:11.3.9"
"@grafana/plugin-configs": "workspace:*"
"@grafana/runtime": "workspace:*"
"@grafana/schema": "workspace:*"
"@grafana/ui": "workspace:*"