Fix CVE-2026-33382, CVE-2026-33376, CVE-2026-8609, CVE-2026-33377

This commit is contained in:
Andrew Lukoshko 2026-08-12 12:09:46 +00:00
parent 97d0b65955
commit eb46b26e46
5 changed files with 348 additions and 1 deletions

View File

@ -0,0 +1,244 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Andrew Lukoshko <alukoshko@almalinux.org>
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

View File

@ -0,0 +1,38 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Andrew Lukoshko <alukoshko@almalinux.org>
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)

View File

@ -0,0 +1,21 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Andrew Lukoshko <alukoshko@almalinux.org>
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)
}

View File

@ -0,0 +1,26 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Andrew Lukoshko <alukoshko@almalinux.org>
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
}

View File

@ -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 <alukoshko@almalinux.org> - 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 <sfeifer@redhat.com> 10.2.6-27
- Resolves: RHEL-183694: CVE-2026-39821