From eb46b26e464fb1dbf3769dc24c9913ccdca7ab96 Mon Sep 17 00:00:00 2001 From: Andrew Lukoshko Date: Wed, 12 Aug 2026 12:09:46 +0000 Subject: [PATCH] Fix CVE-2026-33382, CVE-2026-33376, CVE-2026-8609, CVE-2026-33377 --- 0017-fix-CVE-2026-33382.patch | 244 ++++++++++++++++++++++++++++++++++ 0018-fix-CVE-2026-33376.patch | 38 ++++++ 0019-fix-CVE-2026-8609.patch | 21 +++ 0020-fix-CVE-2026-33377.patch | 26 ++++ grafana.spec | 20 ++- 5 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 0017-fix-CVE-2026-33382.patch create mode 100644 0018-fix-CVE-2026-33376.patch create mode 100644 0019-fix-CVE-2026-8609.patch create mode 100644 0020-fix-CVE-2026-33377.patch diff --git a/0017-fix-CVE-2026-33382.patch b/0017-fix-CVE-2026-33382.patch new file mode 100644 index 0000000..760c2af --- /dev/null +++ b/0017-fix-CVE-2026-33382.patch @@ -0,0 +1,244 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Andrew Lukoshko +Date: Wed, 12 Aug 2026 00:00:00 +0000 +Subject: [PATCH] Cap pre-auth form and public dashboard request bodies (CVE-2026-33382) + +Backported from upstream grafana/grafana commit +42cdc39124912a8506a0c613c319c345aa950b29, with the +pkg/services/publicdashboards/internal/api paths remapped to +pkg/services/publicdashboards/api as they exist in 10.2.6. +--- +diff --git a/pkg/api/login.go b/pkg/api/login.go +index e9e88cf..96723d8 100644 +--- a/pkg/api/login.go ++++ b/pkg/api/login.go +@@ -204,6 +204,9 @@ func (hs *HTTPServer) LoginAPIPing(c *contextmodel.ReqContext) response.Response + } + + func (hs *HTTPServer) LoginPost(c *contextmodel.ReqContext) response.Response { ++ // Cap the request body up-front so any downstream consumer inherits the limit. ++ c.Req.Body = http.MaxBytesReader(c.Resp, c.Req.Body, maxPreAuthFormBodySize) ++ + identity, err := hs.authnService.Login(c.Req.Context(), authn.ClientForm, &authn.Request{HTTPRequest: c.Req, Resp: c.Resp}) + if err != nil { + tokenErr := &auth.CreateTokenErr{} +diff --git a/pkg/api/org_invite.go b/pkg/api/org_invite.go +index ab4f1b9..437f955 100644 +--- a/pkg/api/org_invite.go ++++ b/pkg/api/org_invite.go +@@ -235,6 +235,8 @@ func (hs *HTTPServer) GetInviteInfoByCode(c *contextmodel.ReqContext) response.R + } + + func (hs *HTTPServer) CompleteInvite(c *contextmodel.ReqContext) response.Response { ++ c.Req.Body = http.MaxBytesReader(c.Resp, c.Req.Body, maxPreAuthFormBodySize) ++ + completeInvite := dtos.CompleteInviteForm{} + var err error + if err = web.Bind(c.Req, &completeInvite); err != nil { +diff --git a/pkg/api/password.go b/pkg/api/password.go +index 5567c9a..0ca9c28 100644 +--- a/pkg/api/password.go ++++ b/pkg/api/password.go +@@ -16,6 +16,8 @@ import ( + ) + + func (hs *HTTPServer) SendResetPasswordEmail(c *contextmodel.ReqContext) response.Response { ++ c.Req.Body = http.MaxBytesReader(c.Resp, c.Req.Body, maxPreAuthFormBodySize) ++ + form := dtos.SendResetPasswordEmailForm{} + if err := web.Bind(c.Req, &form); err != nil { + return response.Error(http.StatusBadRequest, "bad request data", err) +@@ -54,6 +56,8 @@ func (hs *HTTPServer) SendResetPasswordEmail(c *contextmodel.ReqContext) respons + } + + func (hs *HTTPServer) ResetPassword(c *contextmodel.ReqContext) response.Response { ++ c.Req.Body = http.MaxBytesReader(c.Resp, c.Req.Body, maxPreAuthFormBodySize) ++ + form := dtos.ResetUserPasswordForm{} + if err := web.Bind(c.Req, &form); err != nil { + return response.Error(http.StatusBadRequest, "bad request data", err) +diff --git a/pkg/api/signup.go b/pkg/api/signup.go +index 0f9310d..b5d20c2 100644 +--- a/pkg/api/signup.go ++++ b/pkg/api/signup.go +@@ -19,6 +19,9 @@ import ( + "github.com/grafana/grafana/pkg/web" + ) + ++// maxPreAuthFormBodySize caps pre-auth form-shaped request bodies (signup, password reset, invite completion, login). ++const maxPreAuthFormBodySize = 100 * 1024 // 100 KiB ++ + // GET /api/user/signup/options + func (hs *HTTPServer) GetSignUpOptions(c *contextmodel.ReqContext) response.Response { + return response.JSON(http.StatusOK, util.DynMap{ +@@ -29,6 +32,8 @@ func (hs *HTTPServer) GetSignUpOptions(c *contextmodel.ReqContext) response.Resp + + // POST /api/user/signup + func (hs *HTTPServer) SignUp(c *contextmodel.ReqContext) response.Response { ++ c.Req.Body = http.MaxBytesReader(c.Resp, c.Req.Body, maxPreAuthFormBodySize) ++ + form := dtos.SignUpForm{} + var err error + if err = web.Bind(c.Req, &form); err != nil { +@@ -82,6 +87,8 @@ func (hs *HTTPServer) SignUp(c *contextmodel.ReqContext) response.Response { + } + + func (hs *HTTPServer) SignUpStep2(c *contextmodel.ReqContext) response.Response { ++ c.Req.Body = http.MaxBytesReader(c.Resp, c.Req.Body, maxPreAuthFormBodySize) ++ + form := dtos.SignUpStep2Form{} + if err := web.Bind(c.Req, &form); err != nil { + return response.Error(http.StatusBadRequest, "bad request data", err) +diff --git a/pkg/services/publicdashboards/api/api.go b/pkg/services/publicdashboards/api/api.go +index 3713e7d..4464cc8 100644 +--- a/pkg/services/publicdashboards/api/api.go ++++ b/pkg/services/publicdashboards/api/api.go +@@ -20,6 +20,12 @@ import ( + "github.com/grafana/grafana/pkg/web" + ) + ++// The struct size the request body unmarshals to is a few bytes and much deviation from that would mean a malformed body. ++const ( ++ maxQueryBodySize = 10 * 1024 // 10 KiB ++ maxMutateBodySize = 10 * 1024 // 10 KiB ++) ++ + type Api struct { + PublicDashboardService publicdashboards.Service + RouteRegister routing.RouteRegister +@@ -176,6 +182,8 @@ func (api *Api) CreatePublicDashboard(c *contextmodel.ReqContext) response.Respo + return response.Err(ErrInvalidUid.Errorf("CreatePublicDashboard: invalid Uid %s", dashboardUid)) + } + ++ c.Req.Body = http.MaxBytesReader(c.Resp, c.Req.Body, maxMutateBodySize) ++ + pdDTO := &PublicDashboardDTO{} + if err := web.Bind(c.Req, pdDTO); err != nil { + return response.Err(ErrBadRequest.Errorf("CreatePublicDashboard: bad request data %v", err)) +@@ -235,6 +243,8 @@ func (api *Api) UpdatePublicDashboard(c *contextmodel.ReqContext) response.Respo + return response.Err(ErrInvalidUid.Errorf("UpdatePublicDashboard: invalid Uid %s", uid)) + } + ++ c.Req.Body = http.MaxBytesReader(c.Resp, c.Req.Body, maxMutateBodySize) ++ + pdDTO := &PublicDashboardDTO{} + if err := web.Bind(c.Req, pdDTO); err != nil { + return response.Err(ErrBadRequest.Errorf("UpdatePublicDashboard: bad request data %v", err)) +diff --git a/pkg/services/publicdashboards/api/query.go b/pkg/services/publicdashboards/api/query.go +index e6dabff..28161c0 100644 +--- a/pkg/services/publicdashboards/api/query.go ++++ b/pkg/services/publicdashboards/api/query.go +@@ -57,6 +57,13 @@ func (api *Api) QueryPublicDashboard(c *contextmodel.ReqContext) response.Respon + return response.Err(ErrInvalidAccessToken.Errorf("QueryPublicDashboard: invalid access token")) + } + ++ _, err := api.PublicDashboardService.FindByAccessToken(c.Req.Context(), accessToken) ++ if err != nil { ++ return response.Err(err) ++ } ++ ++ c.Req.Body = http.MaxBytesReader(c.Resp, c.Req.Body, maxQueryBodySize) ++ + panelId, err := strconv.ParseInt(web.Params(c.Req)[":panelId"], 10, 64) + if err != nil { + return response.Err(ErrInvalidPanelId.Errorf("QueryPublicDashboard: error parsing panelId %v", err)) +diff --git a/pkg/web/binding.go b/pkg/web/binding.go +index ef5d580..6ce059b 100644 +--- a/pkg/web/binding.go ++++ b/pkg/web/binding.go +@@ -10,6 +10,9 @@ import ( + "reflect" + ) + ++// MaxBindBodyBytes caps the size of a JSON request body that Bind will read ++const MaxBindBodyBytes = 100 << 20 ++ + // Bind deserializes JSON payload from the request + func Bind(req *http.Request, v any) error { + if req.Body != nil { +@@ -21,7 +24,8 @@ func Bind(req *http.Request, v any) error { + return errors.New("bad content type") + } + defer func() { _ = req.Body.Close() }() +- err = json.NewDecoder(req.Body).Decode(v) ++ body := http.MaxBytesReader(nil, req.Body, MaxBindBodyBytes) ++ err = json.NewDecoder(body).Decode(v) + if err != nil && !errors.Is(err, io.EOF) { + return err + } +diff --git a/pkg/web/binding_test.go b/pkg/web/binding_test.go +index 4c5d955..f269b93 100644 +--- a/pkg/web/binding_test.go ++++ b/pkg/web/binding_test.go +@@ -2,6 +2,10 @@ package web + + import ( + "errors" ++ "io" ++ "net/http" ++ "net/http/httptest" ++ "strings" + "testing" + ) + +@@ -61,6 +65,60 @@ func (sv *StructWithPointerValidation) Validate() error { + return nil + } + ++type BindTarget struct { ++ Message string `json:"message"` ++} ++ ++// Used to verify that Bind does not try to allocate the whole body on the heap ++type endlessReader struct { ++ prefix []byte ++ read int64 ++} ++ ++func (r *endlessReader) Read(p []byte) (int, error) { ++ if len(r.prefix) > 0 { ++ n := copy(p, r.prefix) ++ r.prefix = r.prefix[n:] ++ r.read += int64(n) ++ return n, nil ++ } ++ for i := range p { ++ p[i] = 'A' ++ } ++ r.read += int64(len(p)) ++ return len(p), nil ++} ++ ++func TestBindRejectsOversizedBody(t *testing.T) { ++ body := &endlessReader{prefix: []byte(`{"message":"`)} ++ req := httptest.NewRequest(http.MethodPost, "/", body) ++ req.Header.Set("Content-Type", "application/json") ++ ++ var target BindTarget ++ err := Bind(req, &target) ++ if err == nil { ++ t.Fatal("expected Bind to reject oversized body, got nil error") ++ } ++ ++ if body.read > MaxBindBodyBytes+(1<<20) { ++ t.Fatalf("Bind read %d bytes, want at most ~%d", body.read, MaxBindBodyBytes) ++ } ++} ++ ++func TestBindAcceptsBodyWithinLimit(t *testing.T) { ++ payload := `{"message":"` + strings.Repeat("A", 1024) + `"}` ++ req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(payload)) ++ req.Header.Set("Content-Type", "application/json") ++ ++ var target BindTarget ++ if err := Bind(req, &target); err != nil && !errors.Is(err, io.EOF) { ++ t.Fatalf("Bind failed on small body: %v", err) ++ } ++ if len(target.Message) != 1024 { ++ t.Fatalf("unexpected message length: got %d, want 1024", len(target.Message)) ++ } ++} ++ + func TestValidationSuccess(t *testing.T) { + var nilInterface *StructWithPointerValidation + diff --git a/0018-fix-CVE-2026-33376.patch b/0018-fix-CVE-2026-33376.patch new file mode 100644 index 0000000..cab47ef --- /dev/null +++ b/0018-fix-CVE-2026-33376.patch @@ -0,0 +1,38 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Andrew Lukoshko +Date: Wed, 12 Aug 2026 00:00:00 +0000 +Subject: [PATCH] Fix auth proxy IPv6 handling (CVE-2026-33376) + +Backported from upstream grafana/grafana commit b4f9ec28ff58. +--- +diff --git a/pkg/services/authn/clients/proxy.go b/pkg/services/authn/clients/proxy.go +index 06abe1d..80a7909 100644 +--- a/pkg/services/authn/clients/proxy.go ++++ b/pkg/services/authn/clients/proxy.go +@@ -6,7 +6,6 @@ import ( + "fmt" + "hash/fnv" + "net" +- "path" + "strconv" + "strings" + "time" +@@ -189,7 +188,17 @@ func parseAcceptList(s string) ([]*net.IPNet, error) { + func coerceProxyAddress(proxyAddr string) (*net.IPNet, error) { + proxyAddr = strings.TrimSpace(proxyAddr) + if !strings.Contains(proxyAddr, "/") { +- proxyAddr = path.Join(proxyAddr, "32") ++ ip := net.ParseIP(proxyAddr) ++ if ip == nil { ++ return nil, fmt.Errorf("could not parse the network: invalid IP address") ++ } ++ ++ mask := 32 ++ if ip.To4() == nil { ++ mask = 128 ++ } ++ ++ proxyAddr = fmt.Sprintf("%s/%d", proxyAddr, mask) + } + + _, network, err := net.ParseCIDR(proxyAddr) diff --git a/0019-fix-CVE-2026-8609.patch b/0019-fix-CVE-2026-8609.patch new file mode 100644 index 0000000..865be1a --- /dev/null +++ b/0019-fix-CVE-2026-8609.patch @@ -0,0 +1,21 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Andrew Lukoshko +Date: Wed, 12 Aug 2026 00:00:00 +0000 +Subject: [PATCH] AuthN: use unknown as failed login metric client label (CVE-2026-8609) + +Backported from upstream grafana/grafana commit +82ef13993059351bf21de35b8488bbd9b42df4f4 (PR #125789). +--- +diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go +index b72fc43..f242d70 100644 +--- a/pkg/services/authn/authnimpl/service.go ++++ b/pkg/services/authn/authnimpl/service.go +@@ -279,7 +279,7 @@ func (s *Service) Login(ctx context.Context, client string, r *authn.Request) (i + + c, ok := s.clients[client] + if !ok { +- s.metrics.failedLogin.WithLabelValues(client).Inc() ++ s.metrics.failedLogin.WithLabelValues("unknown").Inc() + return nil, authn.ErrClientNotConfigured.Errorf("client not configured: %s", client) + } + diff --git a/0020-fix-CVE-2026-33377.patch b/0020-fix-CVE-2026-33377.patch new file mode 100644 index 0000000..ec65d8c --- /dev/null +++ b/0020-fix-CVE-2026-33377.patch @@ -0,0 +1,26 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Andrew Lukoshko +Date: Wed, 12 Aug 2026 00:00:00 +0000 +Subject: [PATCH] Dashboards: only set default permissions for newly created dashboards + +(CVE-2026-33377). + +Backported from upstream grafana/grafana commit +50cdaec1d828f34e8d2962038c3246f1db1ab565. +--- +diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go +index 49ee617..913c479 100644 +--- a/pkg/services/dashboards/service/dashboard_service.go ++++ b/pkg/services/dashboards/service/dashboard_service.go +@@ -482,7 +482,10 @@ func (dr *DashboardServiceImpl) ImportDashboard(ctx context.Context, dto *dashbo + return nil, err + } + +- dr.setDefaultPermissions(ctx, dto, dash, false) ++ // new dashboard created ++ if dto.Dashboard.ID == 0 { ++ dr.setDefaultPermissions(ctx, dto, dash, false) ++ } + + return dash, nil + } diff --git a/grafana.spec b/grafana.spec index 6784b80..6f303f0 100644 --- a/grafana.spec +++ b/grafana.spec @@ -26,7 +26,7 @@ end} Name: grafana Version: 10.2.6 -Release: 27%{?dist} +Release: 28%{?dist}.4 Summary: Metrics dashboard and graph editor License: AGPL-3.0-only URL: https://grafana.org @@ -83,6 +83,13 @@ Patch13: 0013-fix-CVE-2025-4123.patch Patch14: 0014-Fix-CVE-2026-21721.patch Patch15: 0015-Fix-CVE-2026-27877.patch Patch16: 0016-fix-x-net-CVE.patch +# https://github.com/grafana/grafana/commit/42cdc39124912a8506a0c613c319c345aa950b29 +Patch17: 0017-fix-CVE-2026-33382.patch +# https://github.com/grafana/grafana/commit/b4f9ec28ff58 +Patch18: 0018-fix-CVE-2026-33376.patch +Patch19: 0019-fix-CVE-2026-8609.patch +# https://github.com/grafana/grafana/commit/50cdaec1d828f34e8d2962038c3246f1db1ab565 +Patch20: 0020-fix-CVE-2026-33377.patch # Patches affecting the vendor tarball Patch1001: 1001-vendor-patch-removed-backend-crypto.patch @@ -781,6 +788,10 @@ rm -r plugins-bundled %patch -P 14 -p1 %patch -P 15 -p1 %patch -P 16 -p1 +%patch -P 17 -p1 +%patch -P 18 -p1 +%patch -P 19 -p1 +%patch -P 20 -p1 %patch -P 1001 -p1 %if %{enable_fips_mode} @@ -1026,6 +1037,13 @@ done %ghost %verify(not md5 size mode mtime) %{_sharedstatedir}/selinux/*/active/modules/200/grafana %changelog +* Wed Aug 12 2026 Andrew Lukoshko - 10.2.6-28.4 +- Fix CVE-2026-33382: cap pre-auth form and public dashboard request bodies +- Fix CVE-2026-33376: auth proxy IPv6 handling +- Fix CVE-2026-8609: use unknown as failed login metric client label +- Fix CVE-2026-33377: only set default permissions for new dashboards + All backported from upstream grafana/grafana. + * Wed Jul 01 2026 Sam Feifer 10.2.6-27 - Resolves: RHEL-183694: CVE-2026-39821