Fix CVE-2026-33382, CVE-2026-33376, CVE-2026-8609, CVE-2026-33377
This commit is contained in:
parent
97d0b65955
commit
eb46b26e46
244
0017-fix-CVE-2026-33382.patch
Normal file
244
0017-fix-CVE-2026-33382.patch
Normal 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
|
||||
|
||||
38
0018-fix-CVE-2026-33376.patch
Normal file
38
0018-fix-CVE-2026-33376.patch
Normal 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)
|
||||
21
0019-fix-CVE-2026-8609.patch
Normal file
21
0019-fix-CVE-2026-8609.patch
Normal 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)
|
||||
}
|
||||
|
||||
26
0020-fix-CVE-2026-33377.patch
Normal file
26
0020-fix-CVE-2026-33377.patch
Normal 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
|
||||
}
|
||||
20
grafana.spec
20
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 <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
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user