VYPR
High severity7.6GHSA Advisory· Published Jul 18, 2025· Updated Apr 15, 2026

CVE-2025-6023

CVE-2025-6023

Description

An open redirect vulnerability has been identified in Grafana OSS that can be exploited to achieve XSS attacks. The vulnerability was introduced in Grafana v11.5.0.

The open redirect can be chained with path traversal vulnerabilities to achieve XSS.

Fixed in versions 12.0.2+security-01, 11.6.3+security-01, 11.5.6+security-01, 11.4.6+security-01 and 11.3.8+security-01

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/grafana/grafanaGo
< 1.9.2-0.20250521205822-0ba0b99665a91.9.2-0.20250521205822-0ba0b99665a9

Affected products

1

Patches

5
b6dd2b70c655

apply security patch: release-12.0.1/security-patch-202505051005.patch

https://github.com/grafana/grafanagithub-actions[bot]May 22, 2025via ghsa
2 files changed · +180 5
  • pkg/api/static/static.go+6 5 modified
    @@ -159,16 +159,17 @@ func staticHandler(ctx *web.Context, log log.Logger, opt StaticOptions) bool {
     	if fi.IsDir() {
     		// Redirect if missing trailing slash.
     		if !strings.HasSuffix(ctx.Req.URL.Path, "/") {
    -			path := fmt.Sprintf("%s/", ctx.Req.URL.Path)
    -			if !strings.HasPrefix(path, "/") {
    +			redirectPath := path.Clean(ctx.Req.URL.Path)
    +			redirectPath = fmt.Sprintf("%s/", redirectPath)
    +			if !strings.HasPrefix(redirectPath, "/") {
     				// Disambiguate that it's a path relative to this server
    -				path = fmt.Sprintf("/%s", path)
    +				redirectPath = fmt.Sprintf("/%s", redirectPath)
     			} else {
     				// A string starting with // or /\ is interpreted by browsers as a URL, and not a server relative path
     				rePrefix := regexp.MustCompile(`^(?:/\\|/+)`)
    -				path = rePrefix.ReplaceAllString(path, "/")
    +				redirectPath = rePrefix.ReplaceAllString(redirectPath, "/")
     			}
    -			http.Redirect(ctx.Resp, ctx.Req, path, http.StatusFound)
    +			http.Redirect(ctx.Resp, ctx.Req, redirectPath, http.StatusFound)
     			return true
     		}
     
    
  • pkg/api/static/static_test.go+174 0 added
    @@ -0,0 +1,174 @@
    +package httpstatic
    +
    +import (
    +	"io"
    +	"net/http"
    +	"net/http/httptest"
    +	"os"
    +	"path/filepath"
    +	"testing"
    +
    +	claims "github.com/grafana/authlib/types"
    +	"github.com/grafana/grafana/pkg/models/usertoken"
    +	"github.com/grafana/grafana/pkg/services/authn"
    +	"github.com/grafana/grafana/pkg/services/authn/authntest"
    +	"github.com/grafana/grafana/pkg/services/contexthandler"
    +	"github.com/grafana/grafana/pkg/services/featuremgmt"
    +	"github.com/grafana/grafana/pkg/setting"
    +	"github.com/grafana/grafana/pkg/web"
    +	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
    +)
    +
    +func TestStatic(t *testing.T) {
    +	// Create a temporary directory for test files
    +	tmpDir, err := os.MkdirTemp("", "static-test")
    +	require.NoError(t, err)
    +	defer os.RemoveAll(tmpDir)
    +
    +	// Create test files
    +	testFiles := map[string]string{
    +		"test.txt":        "Test content",
    +		"subdir/test.txt": "Subdir content",
    +	}
    +
    +	for path, content := range testFiles {
    +		fullPath := filepath.Join(tmpDir, path)
    +		err := os.MkdirAll(filepath.Dir(fullPath), 0o755)
    +		require.NoError(t, err)
    +		err = os.WriteFile(fullPath, []byte(content), 0o644)
    +		require.NoError(t, err)
    +	}
    +
    +	tests := []struct {
    +		dir              string
    +		name             string
    +		path             string
    +		options          StaticOptions
    +		expectedStatus   int
    +		expectedBody     string
    +		expectedLocation string
    +	}{
    +		{
    +			name:           "should serve existing file",
    +			path:           "/test.txt",
    +			expectedStatus: http.StatusOK,
    +			expectedBody:   "Test content",
    +			dir:            tmpDir,
    +		},
    +		{
    +			name:           "should serve file from subdirectory",
    +			path:           "/subdir/test.txt",
    +			expectedStatus: http.StatusOK,
    +			expectedBody:   "Subdir content",
    +			dir:            tmpDir,
    +		},
    +
    +		{
    +			name:             "should redirect directory without trailing slash",
    +			path:             "/subdir",
    +			expectedStatus:   http.StatusFound,
    +			expectedLocation: "/subdir/",
    +			dir:              tmpDir,
    +		},
    +		{
    +			name:           "should handle prefix",
    +			path:           "/static/test.txt",
    +			options:        StaticOptions{Prefix: "/static"},
    +			expectedStatus: http.StatusOK,
    +			expectedBody:   "Test content",
    +			dir:            tmpDir,
    +		},
    +		{
    +			name:           "should handle excluded path",
    +			path:           "/test.txt",
    +			options:        StaticOptions{Exclude: []string{"/test.txt"}},
    +			expectedStatus: http.StatusNotFound,
    +			dir:            tmpDir,
    +		},
    +		{
    +			name:           "should add custom headers",
    +			path:           "/test.txt",
    +			options:        StaticOptions{AddHeaders: func(ctx *web.Context) { ctx.Resp.Header().Set("X-Test", "test") }},
    +			expectedStatus: http.StatusOK,
    +			expectedBody:   "Test content",
    +			dir:            tmpDir,
    +		},
    +		{
    +			name:             "should clean up path before redirecting",
    +			path:             "/subdir/..%2F%5C127.0.0.1:80%2F%3F%2F..%2F..",
    +			options:          StaticOptions{Prefix: "subdir"},
    +			expectedStatus:   http.StatusFound,
    +			expectedLocation: "/",
    +			dir:              tmpDir,
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			sc := setupScenarioContext(t, "")
    +			sc.m.Use(Static(tt.dir, tt.options))
    +
    +			// Create a test request
    +			req := httptest.NewRequest("GET", tt.path, nil)
    +			w := httptest.NewRecorder()
    +
    +			// Execute the handler
    +			sc.m.ServeHTTP(w, req)
    +
    +			// Verify the response
    +			resp := w.Result()
    +			require.Equal(t, tt.expectedStatus, resp.StatusCode)
    +
    +			if tt.expectedBody != "" {
    +				body, err := io.ReadAll(resp.Body)
    +				require.NoError(t, err)
    +				assert.Equal(t, tt.expectedBody, string(body))
    +			}
    +
    +			if tt.options.AddHeaders != nil {
    +				assert.Equal(t, "test", resp.Header.Get("X-Test"))
    +			}
    +
    +			if tt.expectedLocation != "" {
    +				assert.Equal(t, tt.expectedLocation, resp.Header.Get("Location"))
    +			}
    +		})
    +	}
    +}
    +
    +type scenarioContext struct {
    +	t       *testing.T
    +	cfg     *setting.Cfg
    +	m       *web.Mux
    +	ctxHdlr *contexthandler.ContextHandler
    +}
    +
    +func getContextHandler(t *testing.T, cfg *setting.Cfg) *contexthandler.ContextHandler {
    +	t.Helper()
    +
    +	if cfg == nil {
    +		cfg = setting.NewCfg()
    +	}
    +
    +	return contexthandler.ProvideService(
    +		cfg,
    +		&authntest.FakeService{ExpectedIdentity: &authn.Identity{ID: "0", Type: claims.TypeAnonymous, SessionToken: &usertoken.UserToken{}}},
    +		featuremgmt.WithFeatures(),
    +	)
    +}
    +
    +func setupScenarioContext(t *testing.T, url string) *scenarioContext {
    +	cfg := setting.NewCfg()
    +	ctxHdlr := getContextHandler(t, cfg)
    +	sc := &scenarioContext{
    +		t:       t,
    +		cfg:     cfg,
    +		ctxHdlr: ctxHdlr,
    +	}
    +
    +	sc.m = web.New()
    +	sc.m.Use(ctxHdlr.Middleware)
    +
    +	return sc
    +}
    
e0ba4b480954

apply security patch: release-11.6.2/security-patch-202505051005.patch

https://github.com/grafana/grafanagithub-actions[bot]May 21, 2025via ghsa
2 files changed · +180 5
  • pkg/api/static/static.go+6 5 modified
    @@ -159,16 +159,17 @@ func staticHandler(ctx *web.Context, log log.Logger, opt StaticOptions) bool {
     	if fi.IsDir() {
     		// Redirect if missing trailing slash.
     		if !strings.HasSuffix(ctx.Req.URL.Path, "/") {
    -			path := fmt.Sprintf("%s/", ctx.Req.URL.Path)
    -			if !strings.HasPrefix(path, "/") {
    +			redirectPath := path.Clean(ctx.Req.URL.Path)
    +			redirectPath = fmt.Sprintf("%s/", redirectPath)
    +			if !strings.HasPrefix(redirectPath, "/") {
     				// Disambiguate that it's a path relative to this server
    -				path = fmt.Sprintf("/%s", path)
    +				redirectPath = fmt.Sprintf("/%s", redirectPath)
     			} else {
     				// A string starting with // or /\ is interpreted by browsers as a URL, and not a server relative path
     				rePrefix := regexp.MustCompile(`^(?:/\\|/+)`)
    -				path = rePrefix.ReplaceAllString(path, "/")
    +				redirectPath = rePrefix.ReplaceAllString(redirectPath, "/")
     			}
    -			http.Redirect(ctx.Resp, ctx.Req, path, http.StatusFound)
    +			http.Redirect(ctx.Resp, ctx.Req, redirectPath, http.StatusFound)
     			return true
     		}
     
    
  • pkg/api/static/static_test.go+174 0 added
    @@ -0,0 +1,174 @@
    +package httpstatic
    +
    +import (
    +	"io"
    +	"net/http"
    +	"net/http/httptest"
    +	"os"
    +	"path/filepath"
    +	"testing"
    +
    +	claims "github.com/grafana/authlib/types"
    +	"github.com/grafana/grafana/pkg/models/usertoken"
    +	"github.com/grafana/grafana/pkg/services/authn"
    +	"github.com/grafana/grafana/pkg/services/authn/authntest"
    +	"github.com/grafana/grafana/pkg/services/contexthandler"
    +	"github.com/grafana/grafana/pkg/services/featuremgmt"
    +	"github.com/grafana/grafana/pkg/setting"
    +	"github.com/grafana/grafana/pkg/web"
    +	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
    +)
    +
    +func TestStatic(t *testing.T) {
    +	// Create a temporary directory for test files
    +	tmpDir, err := os.MkdirTemp("", "static-test")
    +	require.NoError(t, err)
    +	defer os.RemoveAll(tmpDir)
    +
    +	// Create test files
    +	testFiles := map[string]string{
    +		"test.txt":        "Test content",
    +		"subdir/test.txt": "Subdir content",
    +	}
    +
    +	for path, content := range testFiles {
    +		fullPath := filepath.Join(tmpDir, path)
    +		err := os.MkdirAll(filepath.Dir(fullPath), 0o755)
    +		require.NoError(t, err)
    +		err = os.WriteFile(fullPath, []byte(content), 0o644)
    +		require.NoError(t, err)
    +	}
    +
    +	tests := []struct {
    +		dir              string
    +		name             string
    +		path             string
    +		options          StaticOptions
    +		expectedStatus   int
    +		expectedBody     string
    +		expectedLocation string
    +	}{
    +		{
    +			name:           "should serve existing file",
    +			path:           "/test.txt",
    +			expectedStatus: http.StatusOK,
    +			expectedBody:   "Test content",
    +			dir:            tmpDir,
    +		},
    +		{
    +			name:           "should serve file from subdirectory",
    +			path:           "/subdir/test.txt",
    +			expectedStatus: http.StatusOK,
    +			expectedBody:   "Subdir content",
    +			dir:            tmpDir,
    +		},
    +
    +		{
    +			name:             "should redirect directory without trailing slash",
    +			path:             "/subdir",
    +			expectedStatus:   http.StatusFound,
    +			expectedLocation: "/subdir/",
    +			dir:              tmpDir,
    +		},
    +		{
    +			name:           "should handle prefix",
    +			path:           "/static/test.txt",
    +			options:        StaticOptions{Prefix: "/static"},
    +			expectedStatus: http.StatusOK,
    +			expectedBody:   "Test content",
    +			dir:            tmpDir,
    +		},
    +		{
    +			name:           "should handle excluded path",
    +			path:           "/test.txt",
    +			options:        StaticOptions{Exclude: []string{"/test.txt"}},
    +			expectedStatus: http.StatusNotFound,
    +			dir:            tmpDir,
    +		},
    +		{
    +			name:           "should add custom headers",
    +			path:           "/test.txt",
    +			options:        StaticOptions{AddHeaders: func(ctx *web.Context) { ctx.Resp.Header().Set("X-Test", "test") }},
    +			expectedStatus: http.StatusOK,
    +			expectedBody:   "Test content",
    +			dir:            tmpDir,
    +		},
    +		{
    +			name:             "should clean up path before redirecting",
    +			path:             "/subdir/..%2F%5C127.0.0.1:80%2F%3F%2F..%2F..",
    +			options:          StaticOptions{Prefix: "subdir"},
    +			expectedStatus:   http.StatusFound,
    +			expectedLocation: "/",
    +			dir:              tmpDir,
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			sc := setupScenarioContext(t, "")
    +			sc.m.Use(Static(tt.dir, tt.options))
    +
    +			// Create a test request
    +			req := httptest.NewRequest("GET", tt.path, nil)
    +			w := httptest.NewRecorder()
    +
    +			// Execute the handler
    +			sc.m.ServeHTTP(w, req)
    +
    +			// Verify the response
    +			resp := w.Result()
    +			require.Equal(t, tt.expectedStatus, resp.StatusCode)
    +
    +			if tt.expectedBody != "" {
    +				body, err := io.ReadAll(resp.Body)
    +				require.NoError(t, err)
    +				assert.Equal(t, tt.expectedBody, string(body))
    +			}
    +
    +			if tt.options.AddHeaders != nil {
    +				assert.Equal(t, "test", resp.Header.Get("X-Test"))
    +			}
    +
    +			if tt.expectedLocation != "" {
    +				assert.Equal(t, tt.expectedLocation, resp.Header.Get("Location"))
    +			}
    +		})
    +	}
    +}
    +
    +type scenarioContext struct {
    +	t       *testing.T
    +	cfg     *setting.Cfg
    +	m       *web.Mux
    +	ctxHdlr *contexthandler.ContextHandler
    +}
    +
    +func getContextHandler(t *testing.T, cfg *setting.Cfg) *contexthandler.ContextHandler {
    +	t.Helper()
    +
    +	if cfg == nil {
    +		cfg = setting.NewCfg()
    +	}
    +
    +	return contexthandler.ProvideService(
    +		cfg,
    +		&authntest.FakeService{ExpectedIdentity: &authn.Identity{ID: "0", Type: claims.TypeAnonymous, SessionToken: &usertoken.UserToken{}}},
    +		featuremgmt.WithFeatures(),
    +	)
    +}
    +
    +func setupScenarioContext(t *testing.T, url string) *scenarioContext {
    +	cfg := setting.NewCfg()
    +	ctxHdlr := getContextHandler(t, cfg)
    +	sc := &scenarioContext{
    +		t:       t,
    +		cfg:     cfg,
    +		ctxHdlr: ctxHdlr,
    +	}
    +
    +	sc.m = web.New()
    +	sc.m.Use(ctxHdlr.Middleware)
    +
    +	return sc
    +}
    
5b00e21638f5

apply security patch: release-11.4.5/security-patch-202505051005.patch

https://github.com/grafana/grafanagithub-actions[bot]May 21, 2025via ghsa
2 files changed · +180 5
  • pkg/api/static/static.go+6 5 modified
    @@ -159,16 +159,17 @@ func staticHandler(ctx *web.Context, log log.Logger, opt StaticOptions) bool {
     	if fi.IsDir() {
     		// Redirect if missing trailing slash.
     		if !strings.HasSuffix(ctx.Req.URL.Path, "/") {
    -			path := fmt.Sprintf("%s/", ctx.Req.URL.Path)
    -			if !strings.HasPrefix(path, "/") {
    +			redirectPath := path.Clean(ctx.Req.URL.Path)
    +			redirectPath = fmt.Sprintf("%s/", redirectPath)
    +			if !strings.HasPrefix(redirectPath, "/") {
     				// Disambiguate that it's a path relative to this server
    -				path = fmt.Sprintf("/%s", path)
    +				redirectPath = fmt.Sprintf("/%s", redirectPath)
     			} else {
     				// A string starting with // or /\ is interpreted by browsers as a URL, and not a server relative path
     				rePrefix := regexp.MustCompile(`^(?:/\\|/+)`)
    -				path = rePrefix.ReplaceAllString(path, "/")
    +				redirectPath = rePrefix.ReplaceAllString(redirectPath, "/")
     			}
    -			http.Redirect(ctx.Resp, ctx.Req, path, http.StatusFound)
    +			http.Redirect(ctx.Resp, ctx.Req, redirectPath, http.StatusFound)
     			return true
     		}
     
    
  • pkg/api/static/static_test.go+174 0 added
    @@ -0,0 +1,174 @@
    +package httpstatic
    +
    +import (
    +	"io"
    +	"net/http"
    +	"net/http/httptest"
    +	"os"
    +	"path/filepath"
    +	"testing"
    +
    +	claims "github.com/grafana/authlib/types"
    +	"github.com/grafana/grafana/pkg/models/usertoken"
    +	"github.com/grafana/grafana/pkg/services/authn"
    +	"github.com/grafana/grafana/pkg/services/authn/authntest"
    +	"github.com/grafana/grafana/pkg/services/contexthandler"
    +	"github.com/grafana/grafana/pkg/services/featuremgmt"
    +	"github.com/grafana/grafana/pkg/setting"
    +	"github.com/grafana/grafana/pkg/web"
    +	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
    +)
    +
    +func TestStatic(t *testing.T) {
    +	// Create a temporary directory for test files
    +	tmpDir, err := os.MkdirTemp("", "static-test")
    +	require.NoError(t, err)
    +	defer os.RemoveAll(tmpDir)
    +
    +	// Create test files
    +	testFiles := map[string]string{
    +		"test.txt":        "Test content",
    +		"subdir/test.txt": "Subdir content",
    +	}
    +
    +	for path, content := range testFiles {
    +		fullPath := filepath.Join(tmpDir, path)
    +		err := os.MkdirAll(filepath.Dir(fullPath), 0o755)
    +		require.NoError(t, err)
    +		err = os.WriteFile(fullPath, []byte(content), 0o644)
    +		require.NoError(t, err)
    +	}
    +
    +	tests := []struct {
    +		dir              string
    +		name             string
    +		path             string
    +		options          StaticOptions
    +		expectedStatus   int
    +		expectedBody     string
    +		expectedLocation string
    +	}{
    +		{
    +			name:           "should serve existing file",
    +			path:           "/test.txt",
    +			expectedStatus: http.StatusOK,
    +			expectedBody:   "Test content",
    +			dir:            tmpDir,
    +		},
    +		{
    +			name:           "should serve file from subdirectory",
    +			path:           "/subdir/test.txt",
    +			expectedStatus: http.StatusOK,
    +			expectedBody:   "Subdir content",
    +			dir:            tmpDir,
    +		},
    +
    +		{
    +			name:             "should redirect directory without trailing slash",
    +			path:             "/subdir",
    +			expectedStatus:   http.StatusFound,
    +			expectedLocation: "/subdir/",
    +			dir:              tmpDir,
    +		},
    +		{
    +			name:           "should handle prefix",
    +			path:           "/static/test.txt",
    +			options:        StaticOptions{Prefix: "/static"},
    +			expectedStatus: http.StatusOK,
    +			expectedBody:   "Test content",
    +			dir:            tmpDir,
    +		},
    +		{
    +			name:           "should handle excluded path",
    +			path:           "/test.txt",
    +			options:        StaticOptions{Exclude: []string{"/test.txt"}},
    +			expectedStatus: http.StatusNotFound,
    +			dir:            tmpDir,
    +		},
    +		{
    +			name:           "should add custom headers",
    +			path:           "/test.txt",
    +			options:        StaticOptions{AddHeaders: func(ctx *web.Context) { ctx.Resp.Header().Set("X-Test", "test") }},
    +			expectedStatus: http.StatusOK,
    +			expectedBody:   "Test content",
    +			dir:            tmpDir,
    +		},
    +		{
    +			name:             "should clean up path before redirecting",
    +			path:             "/subdir/..%2F%5C127.0.0.1:80%2F%3F%2F..%2F..",
    +			options:          StaticOptions{Prefix: "subdir"},
    +			expectedStatus:   http.StatusFound,
    +			expectedLocation: "/",
    +			dir:              tmpDir,
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			sc := setupScenarioContext(t, "")
    +			sc.m.Use(Static(tt.dir, tt.options))
    +
    +			// Create a test request
    +			req := httptest.NewRequest("GET", tt.path, nil)
    +			w := httptest.NewRecorder()
    +
    +			// Execute the handler
    +			sc.m.ServeHTTP(w, req)
    +
    +			// Verify the response
    +			resp := w.Result()
    +			require.Equal(t, tt.expectedStatus, resp.StatusCode)
    +
    +			if tt.expectedBody != "" {
    +				body, err := io.ReadAll(resp.Body)
    +				require.NoError(t, err)
    +				assert.Equal(t, tt.expectedBody, string(body))
    +			}
    +
    +			if tt.options.AddHeaders != nil {
    +				assert.Equal(t, "test", resp.Header.Get("X-Test"))
    +			}
    +
    +			if tt.expectedLocation != "" {
    +				assert.Equal(t, tt.expectedLocation, resp.Header.Get("Location"))
    +			}
    +		})
    +	}
    +}
    +
    +type scenarioContext struct {
    +	t       *testing.T
    +	cfg     *setting.Cfg
    +	m       *web.Mux
    +	ctxHdlr *contexthandler.ContextHandler
    +}
    +
    +func getContextHandler(t *testing.T, cfg *setting.Cfg) *contexthandler.ContextHandler {
    +	t.Helper()
    +
    +	if cfg == nil {
    +		cfg = setting.NewCfg()
    +	}
    +
    +	return contexthandler.ProvideService(
    +		cfg,
    +		&authntest.FakeService{ExpectedIdentity: &authn.Identity{ID: "0", Type: claims.TypeAnonymous, SessionToken: &usertoken.UserToken{}}},
    +		featuremgmt.WithFeatures(),
    +	)
    +}
    +
    +func setupScenarioContext(t *testing.T, url string) *scenarioContext {
    +	cfg := setting.NewCfg()
    +	ctxHdlr := getContextHandler(t, cfg)
    +	sc := &scenarioContext{
    +		t:       t,
    +		cfg:     cfg,
    +		ctxHdlr: ctxHdlr,
    +	}
    +
    +	sc.m = web.New()
    +	sc.m.Use(ctxHdlr.Middleware)
    +
    +	return sc
    +}
    
40ed88fe86d3

apply security patch: release-11.5.5/security-patch-202505051005.patch

https://github.com/grafana/grafanagithub-actions[bot]May 21, 2025via ghsa
2 files changed · +180 5
  • pkg/api/static/static.go+6 5 modified
    @@ -159,16 +159,17 @@ func staticHandler(ctx *web.Context, log log.Logger, opt StaticOptions) bool {
     	if fi.IsDir() {
     		// Redirect if missing trailing slash.
     		if !strings.HasSuffix(ctx.Req.URL.Path, "/") {
    -			path := fmt.Sprintf("%s/", ctx.Req.URL.Path)
    -			if !strings.HasPrefix(path, "/") {
    +			redirectPath := path.Clean(ctx.Req.URL.Path)
    +			redirectPath = fmt.Sprintf("%s/", redirectPath)
    +			if !strings.HasPrefix(redirectPath, "/") {
     				// Disambiguate that it's a path relative to this server
    -				path = fmt.Sprintf("/%s", path)
    +				redirectPath = fmt.Sprintf("/%s", redirectPath)
     			} else {
     				// A string starting with // or /\ is interpreted by browsers as a URL, and not a server relative path
     				rePrefix := regexp.MustCompile(`^(?:/\\|/+)`)
    -				path = rePrefix.ReplaceAllString(path, "/")
    +				redirectPath = rePrefix.ReplaceAllString(redirectPath, "/")
     			}
    -			http.Redirect(ctx.Resp, ctx.Req, path, http.StatusFound)
    +			http.Redirect(ctx.Resp, ctx.Req, redirectPath, http.StatusFound)
     			return true
     		}
     
    
  • pkg/api/static/static_test.go+174 0 added
    @@ -0,0 +1,174 @@
    +package httpstatic
    +
    +import (
    +	"io"
    +	"net/http"
    +	"net/http/httptest"
    +	"os"
    +	"path/filepath"
    +	"testing"
    +
    +	claims "github.com/grafana/authlib/types"
    +	"github.com/grafana/grafana/pkg/models/usertoken"
    +	"github.com/grafana/grafana/pkg/services/authn"
    +	"github.com/grafana/grafana/pkg/services/authn/authntest"
    +	"github.com/grafana/grafana/pkg/services/contexthandler"
    +	"github.com/grafana/grafana/pkg/services/featuremgmt"
    +	"github.com/grafana/grafana/pkg/setting"
    +	"github.com/grafana/grafana/pkg/web"
    +	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
    +)
    +
    +func TestStatic(t *testing.T) {
    +	// Create a temporary directory for test files
    +	tmpDir, err := os.MkdirTemp("", "static-test")
    +	require.NoError(t, err)
    +	defer os.RemoveAll(tmpDir)
    +
    +	// Create test files
    +	testFiles := map[string]string{
    +		"test.txt":        "Test content",
    +		"subdir/test.txt": "Subdir content",
    +	}
    +
    +	for path, content := range testFiles {
    +		fullPath := filepath.Join(tmpDir, path)
    +		err := os.MkdirAll(filepath.Dir(fullPath), 0o755)
    +		require.NoError(t, err)
    +		err = os.WriteFile(fullPath, []byte(content), 0o644)
    +		require.NoError(t, err)
    +	}
    +
    +	tests := []struct {
    +		dir              string
    +		name             string
    +		path             string
    +		options          StaticOptions
    +		expectedStatus   int
    +		expectedBody     string
    +		expectedLocation string
    +	}{
    +		{
    +			name:           "should serve existing file",
    +			path:           "/test.txt",
    +			expectedStatus: http.StatusOK,
    +			expectedBody:   "Test content",
    +			dir:            tmpDir,
    +		},
    +		{
    +			name:           "should serve file from subdirectory",
    +			path:           "/subdir/test.txt",
    +			expectedStatus: http.StatusOK,
    +			expectedBody:   "Subdir content",
    +			dir:            tmpDir,
    +		},
    +
    +		{
    +			name:             "should redirect directory without trailing slash",
    +			path:             "/subdir",
    +			expectedStatus:   http.StatusFound,
    +			expectedLocation: "/subdir/",
    +			dir:              tmpDir,
    +		},
    +		{
    +			name:           "should handle prefix",
    +			path:           "/static/test.txt",
    +			options:        StaticOptions{Prefix: "/static"},
    +			expectedStatus: http.StatusOK,
    +			expectedBody:   "Test content",
    +			dir:            tmpDir,
    +		},
    +		{
    +			name:           "should handle excluded path",
    +			path:           "/test.txt",
    +			options:        StaticOptions{Exclude: []string{"/test.txt"}},
    +			expectedStatus: http.StatusNotFound,
    +			dir:            tmpDir,
    +		},
    +		{
    +			name:           "should add custom headers",
    +			path:           "/test.txt",
    +			options:        StaticOptions{AddHeaders: func(ctx *web.Context) { ctx.Resp.Header().Set("X-Test", "test") }},
    +			expectedStatus: http.StatusOK,
    +			expectedBody:   "Test content",
    +			dir:            tmpDir,
    +		},
    +		{
    +			name:             "should clean up path before redirecting",
    +			path:             "/subdir/..%2F%5C127.0.0.1:80%2F%3F%2F..%2F..",
    +			options:          StaticOptions{Prefix: "subdir"},
    +			expectedStatus:   http.StatusFound,
    +			expectedLocation: "/",
    +			dir:              tmpDir,
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			sc := setupScenarioContext(t, "")
    +			sc.m.Use(Static(tt.dir, tt.options))
    +
    +			// Create a test request
    +			req := httptest.NewRequest("GET", tt.path, nil)
    +			w := httptest.NewRecorder()
    +
    +			// Execute the handler
    +			sc.m.ServeHTTP(w, req)
    +
    +			// Verify the response
    +			resp := w.Result()
    +			require.Equal(t, tt.expectedStatus, resp.StatusCode)
    +
    +			if tt.expectedBody != "" {
    +				body, err := io.ReadAll(resp.Body)
    +				require.NoError(t, err)
    +				assert.Equal(t, tt.expectedBody, string(body))
    +			}
    +
    +			if tt.options.AddHeaders != nil {
    +				assert.Equal(t, "test", resp.Header.Get("X-Test"))
    +			}
    +
    +			if tt.expectedLocation != "" {
    +				assert.Equal(t, tt.expectedLocation, resp.Header.Get("Location"))
    +			}
    +		})
    +	}
    +}
    +
    +type scenarioContext struct {
    +	t       *testing.T
    +	cfg     *setting.Cfg
    +	m       *web.Mux
    +	ctxHdlr *contexthandler.ContextHandler
    +}
    +
    +func getContextHandler(t *testing.T, cfg *setting.Cfg) *contexthandler.ContextHandler {
    +	t.Helper()
    +
    +	if cfg == nil {
    +		cfg = setting.NewCfg()
    +	}
    +
    +	return contexthandler.ProvideService(
    +		cfg,
    +		&authntest.FakeService{ExpectedIdentity: &authn.Identity{ID: "0", Type: claims.TypeAnonymous, SessionToken: &usertoken.UserToken{}}},
    +		featuremgmt.WithFeatures(),
    +	)
    +}
    +
    +func setupScenarioContext(t *testing.T, url string) *scenarioContext {
    +	cfg := setting.NewCfg()
    +	ctxHdlr := getContextHandler(t, cfg)
    +	sc := &scenarioContext{
    +		t:       t,
    +		cfg:     cfg,
    +		ctxHdlr: ctxHdlr,
    +	}
    +
    +	sc.m = web.New()
    +	sc.m.Use(ctxHdlr.Middleware)
    +
    +	return sc
    +}
    
0ba0b99665a9

apply security patch: release-11.3.7/security-patch-202505051005.patch

https://github.com/grafana/grafanagithub-actions[bot]May 21, 2025via ghsa
2 files changed · +180 5
  • pkg/api/static/static.go+6 5 modified
    @@ -159,16 +159,17 @@ func staticHandler(ctx *web.Context, log log.Logger, opt StaticOptions) bool {
     	if fi.IsDir() {
     		// Redirect if missing trailing slash.
     		if !strings.HasSuffix(ctx.Req.URL.Path, "/") {
    -			path := fmt.Sprintf("%s/", ctx.Req.URL.Path)
    -			if !strings.HasPrefix(path, "/") {
    +			redirectPath := path.Clean(ctx.Req.URL.Path)
    +			redirectPath = fmt.Sprintf("%s/", redirectPath)
    +			if !strings.HasPrefix(redirectPath, "/") {
     				// Disambiguate that it's a path relative to this server
    -				path = fmt.Sprintf("/%s", path)
    +				redirectPath = fmt.Sprintf("/%s", redirectPath)
     			} else {
     				// A string starting with // or /\ is interpreted by browsers as a URL, and not a server relative path
     				rePrefix := regexp.MustCompile(`^(?:/\\|/+)`)
    -				path = rePrefix.ReplaceAllString(path, "/")
    +				redirectPath = rePrefix.ReplaceAllString(redirectPath, "/")
     			}
    -			http.Redirect(ctx.Resp, ctx.Req, path, http.StatusFound)
    +			http.Redirect(ctx.Resp, ctx.Req, redirectPath, http.StatusFound)
     			return true
     		}
     
    
  • pkg/api/static/static_test.go+174 0 added
    @@ -0,0 +1,174 @@
    +package httpstatic
    +
    +import (
    +	"io"
    +	"net/http"
    +	"net/http/httptest"
    +	"os"
    +	"path/filepath"
    +	"testing"
    +
    +	claims "github.com/grafana/authlib/types"
    +	"github.com/grafana/grafana/pkg/models/usertoken"
    +	"github.com/grafana/grafana/pkg/services/authn"
    +	"github.com/grafana/grafana/pkg/services/authn/authntest"
    +	"github.com/grafana/grafana/pkg/services/contexthandler"
    +	"github.com/grafana/grafana/pkg/services/featuremgmt"
    +	"github.com/grafana/grafana/pkg/setting"
    +	"github.com/grafana/grafana/pkg/web"
    +	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
    +)
    +
    +func TestStatic(t *testing.T) {
    +	// Create a temporary directory for test files
    +	tmpDir, err := os.MkdirTemp("", "static-test")
    +	require.NoError(t, err)
    +	defer os.RemoveAll(tmpDir)
    +
    +	// Create test files
    +	testFiles := map[string]string{
    +		"test.txt":        "Test content",
    +		"subdir/test.txt": "Subdir content",
    +	}
    +
    +	for path, content := range testFiles {
    +		fullPath := filepath.Join(tmpDir, path)
    +		err := os.MkdirAll(filepath.Dir(fullPath), 0o755)
    +		require.NoError(t, err)
    +		err = os.WriteFile(fullPath, []byte(content), 0o644)
    +		require.NoError(t, err)
    +	}
    +
    +	tests := []struct {
    +		dir              string
    +		name             string
    +		path             string
    +		options          StaticOptions
    +		expectedStatus   int
    +		expectedBody     string
    +		expectedLocation string
    +	}{
    +		{
    +			name:           "should serve existing file",
    +			path:           "/test.txt",
    +			expectedStatus: http.StatusOK,
    +			expectedBody:   "Test content",
    +			dir:            tmpDir,
    +		},
    +		{
    +			name:           "should serve file from subdirectory",
    +			path:           "/subdir/test.txt",
    +			expectedStatus: http.StatusOK,
    +			expectedBody:   "Subdir content",
    +			dir:            tmpDir,
    +		},
    +
    +		{
    +			name:             "should redirect directory without trailing slash",
    +			path:             "/subdir",
    +			expectedStatus:   http.StatusFound,
    +			expectedLocation: "/subdir/",
    +			dir:              tmpDir,
    +		},
    +		{
    +			name:           "should handle prefix",
    +			path:           "/static/test.txt",
    +			options:        StaticOptions{Prefix: "/static"},
    +			expectedStatus: http.StatusOK,
    +			expectedBody:   "Test content",
    +			dir:            tmpDir,
    +		},
    +		{
    +			name:           "should handle excluded path",
    +			path:           "/test.txt",
    +			options:        StaticOptions{Exclude: []string{"/test.txt"}},
    +			expectedStatus: http.StatusNotFound,
    +			dir:            tmpDir,
    +		},
    +		{
    +			name:           "should add custom headers",
    +			path:           "/test.txt",
    +			options:        StaticOptions{AddHeaders: func(ctx *web.Context) { ctx.Resp.Header().Set("X-Test", "test") }},
    +			expectedStatus: http.StatusOK,
    +			expectedBody:   "Test content",
    +			dir:            tmpDir,
    +		},
    +		{
    +			name:             "should clean up path before redirecting",
    +			path:             "/subdir/..%2F%5C127.0.0.1:80%2F%3F%2F..%2F..",
    +			options:          StaticOptions{Prefix: "subdir"},
    +			expectedStatus:   http.StatusFound,
    +			expectedLocation: "/",
    +			dir:              tmpDir,
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			sc := setupScenarioContext(t, "")
    +			sc.m.Use(Static(tt.dir, tt.options))
    +
    +			// Create a test request
    +			req := httptest.NewRequest("GET", tt.path, nil)
    +			w := httptest.NewRecorder()
    +
    +			// Execute the handler
    +			sc.m.ServeHTTP(w, req)
    +
    +			// Verify the response
    +			resp := w.Result()
    +			require.Equal(t, tt.expectedStatus, resp.StatusCode)
    +
    +			if tt.expectedBody != "" {
    +				body, err := io.ReadAll(resp.Body)
    +				require.NoError(t, err)
    +				assert.Equal(t, tt.expectedBody, string(body))
    +			}
    +
    +			if tt.options.AddHeaders != nil {
    +				assert.Equal(t, "test", resp.Header.Get("X-Test"))
    +			}
    +
    +			if tt.expectedLocation != "" {
    +				assert.Equal(t, tt.expectedLocation, resp.Header.Get("Location"))
    +			}
    +		})
    +	}
    +}
    +
    +type scenarioContext struct {
    +	t       *testing.T
    +	cfg     *setting.Cfg
    +	m       *web.Mux
    +	ctxHdlr *contexthandler.ContextHandler
    +}
    +
    +func getContextHandler(t *testing.T, cfg *setting.Cfg) *contexthandler.ContextHandler {
    +	t.Helper()
    +
    +	if cfg == nil {
    +		cfg = setting.NewCfg()
    +	}
    +
    +	return contexthandler.ProvideService(
    +		cfg,
    +		&authntest.FakeService{ExpectedIdentity: &authn.Identity{ID: "0", Type: claims.TypeAnonymous, SessionToken: &usertoken.UserToken{}}},
    +		featuremgmt.WithFeatures(),
    +	)
    +}
    +
    +func setupScenarioContext(t *testing.T, url string) *scenarioContext {
    +	cfg := setting.NewCfg()
    +	ctxHdlr := getContextHandler(t, cfg)
    +	sc := &scenarioContext{
    +		t:       t,
    +		cfg:     cfg,
    +		ctxHdlr: ctxHdlr,
    +	}
    +
    +	sc.m = web.New()
    +	sc.m.Use(ctxHdlr.Middleware)
    +
    +	return sc
    +}
    

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

11

News mentions

0

No linked articles in our index yet.