File CVE-2026-33186.patch of Package amazon-cloudwatch-agent

From 89d1a70cc1b25ec820344760ab9aed24ef81cb26 Mon Sep 17 00:00:00 2001
From: Easwar Swaminathan <easwars@google.com>
Date: Tue, 17 Mar 2026 16:35:32 -0700
Subject: [PATCH] grpc: enforce strict path checking for incoming requests on
 the server (#8985)

RELEASE NOTES:
* server: fix an authorization bypass where malformed :path headers
(missing the leading slash) could bypass path-based restricted "deny"
rules in interceptors like `grpc/authz`. Any request with a
non-canonical path is now immediately rejected with an `Unimplemented`
error.
---
 internal/envconfig/envconfig.go |  16 +++
 server.go                       |  57 +++++++---
 test/malformed_method_test.go   | 177 ++++++++++++++++++++++++++++++++
 3 files changed, 234 insertions(+), 16 deletions(-)
 create mode 100644 test/malformed_method_test.go

diff --git a/internal/envconfig/envconfig.go b/internal/envconfig/envconfig.go
index cc5713fd..ea7cdd97 100644
--- a/internal/envconfig/envconfig.go
+++ b/internal/envconfig/envconfig.go
@@ -69,6 +69,22 @@ var (
 	// to gRFC A76. It can be enabled by setting the environment variable
 	// "GRPC_EXPERIMENTAL_RING_HASH_SET_REQUEST_HASH_KEY" to "true".
 	RingHashSetRequestHashKey = boolFromEnv("GRPC_EXPERIMENTAL_RING_HASH_SET_REQUEST_HASH_KEY", false)
+
+	// DisableStrictPathChecking indicates whether strict path checking is
+	// disabled. This feature can be disabled by setting the environment
+	// variable GRPC_GO_EXPERIMENTAL_DISABLE_STRICT_PATH_CHECKING to "true".
+	//
+	// When strict path checking is enabled, gRPC will reject requests with
+	// paths that do not conform to the gRPC over HTTP/2 specification found at
+	// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md.
+	//
+	// When disabled, gRPC will allow paths that do not contain a leading slash.
+	// Enabling strict path checking is recommended for security reasons, as it
+	// prevents potential path traversal vulnerabilities.
+	//
+	// A future release will remove this environment variable, enabling strict
+	// path checking behavior unconditionally.
+	DisableStrictPathChecking = boolFromEnv("GRPC_GO_EXPERIMENTAL_DISABLE_STRICT_PATH_CHECKING", false)
 )
 
 func boolFromEnv(envVar string, def bool) bool {
diff --git a/server.go b/server.go
index 976e70ae..e62d3532 100644
--- a/server.go
+++ b/server.go
@@ -42,6 +42,7 @@ import (
 	"google.golang.org/grpc/internal"
 	"google.golang.org/grpc/internal/binarylog"
 	"google.golang.org/grpc/internal/channelz"
+	"google.golang.org/grpc/internal/envconfig"
 	"google.golang.org/grpc/internal/grpcsync"
 	"google.golang.org/grpc/internal/grpcutil"
 	istats "google.golang.org/grpc/internal/stats"
@@ -148,6 +149,8 @@ type Server struct {
 
 	serverWorkerChannel      chan func()
 	serverWorkerChannelClose func()
+
+	strictPathCheckingLogEmitted atomic.Bool
 }
 
 type serverOptions struct {
@@ -1745,6 +1748,24 @@ func (s *Server) processStreamingRPC(ctx context.Context, stream *transport.Serv
 	return ss.s.WriteStatus(statusOK)
 }
 
+func (s *Server) handleMalformedMethodName(stream *transport.ServerStream, ti *traceInfo) {
+	if ti != nil {
+		ti.tr.LazyLog(&fmtStringer{"Malformed method name %q", []any{stream.Method()}}, true)
+		ti.tr.SetError()
+	}
+	errDesc := fmt.Sprintf("malformed method name: %q", stream.Method())
+	if err := stream.WriteStatus(status.New(codes.Unimplemented, errDesc)); err != nil {
+		if ti != nil {
+			ti.tr.LazyLog(&fmtStringer{"%v", []any{err}}, true)
+			ti.tr.SetError()
+		}
+		channelz.Warningf(logger, s.channelz, "grpc: Server.handleStream failed to write status: %v", err)
+	}
+	if ti != nil {
+		ti.tr.Finish()
+	}
+}
+
 func (s *Server) handleStream(t transport.ServerTransport, stream *transport.ServerStream) {
 	ctx := stream.Context()
 	ctx = contextWithServer(ctx, s)
@@ -1765,26 +1786,30 @@ func (s *Server) handleStream(t transport.ServerTransport, stream *transport.Ser
 	}
 
 	sm := stream.Method()
-	if sm != "" && sm[0] == '/' {
+	if sm == "" {
+		s.handleMalformedMethodName(stream, ti)
+		return
+	}
+	if sm[0] != '/' {
+		// TODO(easwars): Add a link to the CVE in the below log messages once
+		// published.
+		if envconfig.DisableStrictPathChecking {
+			if old := s.strictPathCheckingLogEmitted.Swap(true); !old {
+				channelz.Warningf(logger, s.channelz, "grpc: Server.handleStream received malformed method name %q. Allowing it because the environment variable GRPC_GO_EXPERIMENTAL_DISABLE_STRICT_PATH_CHECKING is set to true, but this option will be removed in a future release.", sm)
+			}
+		} else {
+			if old := s.strictPathCheckingLogEmitted.Swap(true); !old {
+				channelz.Warningf(logger, s.channelz, "grpc: Server.handleStream rejected malformed method name %q. To temporarily allow such requests, set the environment variable GRPC_GO_EXPERIMENTAL_DISABLE_STRICT_PATH_CHECKING to true. Note that this is not recommended as it may allow requests to bypass security policies.", sm)
+			}
+			s.handleMalformedMethodName(stream, ti)
+			return
+		}
+	} else {
 		sm = sm[1:]
 	}
 	pos := strings.LastIndex(sm, "/")
 	if pos == -1 {
-		if ti != nil {
-			ti.tr.LazyLog(&fmtStringer{"Malformed method name %q", []any{sm}}, true)
-			ti.tr.SetError()
-		}
-		errDesc := fmt.Sprintf("malformed method name: %q", stream.Method())
-		if err := stream.WriteStatus(status.New(codes.Unimplemented, errDesc)); err != nil {
-			if ti != nil {
-				ti.tr.LazyLog(&fmtStringer{"%v", []any{err}}, true)
-				ti.tr.SetError()
-			}
-			channelz.Warningf(logger, s.channelz, "grpc: Server.handleStream failed to write status: %v", err)
-		}
-		if ti != nil {
-			ti.tr.Finish()
-		}
+		s.handleMalformedMethodName(stream, ti)
 		return
 	}
 	service := sm[:pos]
diff --git a/test/malformed_method_test.go b/test/malformed_method_test.go
new file mode 100644
index 00000000..00e391a2
--- /dev/null
+++ b/test/malformed_method_test.go
@@ -0,0 +1,177 @@
+/*
+ *
+ * Copyright 2026 gRPC authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package test
+
+import (
+	"bytes"
+	"context"
+	"net"
+	"testing"
+
+	"golang.org/x/net/http2"
+	"golang.org/x/net/http2/hpack"
+	"google.golang.org/grpc/internal/envconfig"
+	"google.golang.org/grpc/internal/stubserver"
+	"google.golang.org/grpc/internal/testutils"
+
+	testpb "google.golang.org/grpc/interop/grpc_testing"
+)
+
+// TestMalformedMethodPath tests that the server responds with Unimplemented
+// when the method path is malformed. This verifies that the server does not
+// route requests with a malformed method path to the application handler.
+func (s) TestMalformedMethodPath(t *testing.T) {
+	tests := []struct {
+		name       string
+		path       string
+		envVar     bool
+		wantStatus string // string representation of codes.Code
+	}{
+		{
+			name:       "missing_leading_slash_disableStrictPathChecking_false",
+			path:       "grpc.testing.TestService/UnaryCall",
+			wantStatus: "12", // Unimplemented
+		},
+		{
+			name:       "empty_path_disableStrictPathChecking_false",
+			path:       "",
+			wantStatus: "12", // Unimplemented
+		},
+		{
+			name:       "just_slash_disableStrictPathChecking_false",
+			path:       "/",
+			wantStatus: "12", // Unimplemented
+		},
+		{
+			name:       "missing_leading_slash_disableStrictPathChecking_true",
+			path:       "grpc.testing.TestService/UnaryCall",
+			envVar:     true,
+			wantStatus: "0", // OK
+		},
+		{
+			name:       "empty_path_disableStrictPathChecking_true",
+			path:       "",
+			envVar:     true,
+			wantStatus: "12", // Unimplemented
+		},
+		{
+			name:       "just_slash_disableStrictPathChecking_true",
+			path:       "/",
+			envVar:     true,
+			wantStatus: "12", // Unimplemented
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
+			defer cancel()
+
+			testutils.SetEnvConfig(t, &envconfig.DisableStrictPathChecking, tc.envVar)
+
+			ss := &stubserver.StubServer{
+				UnaryCallF: func(context.Context, *testpb.SimpleRequest) (*testpb.SimpleResponse, error) {
+					return &testpb.SimpleResponse{Payload: &testpb.Payload{Body: []byte("pwned")}}, nil
+				},
+			}
+			if err := ss.Start(nil); err != nil {
+				t.Fatalf("Error starting endpoint server: %v", err)
+			}
+			defer ss.Stop()
+
+			// Open a raw TCP connection to the server and speak HTTP/2 directly.
+			tcpConn, err := net.Dial("tcp", ss.Address)
+			if err != nil {
+				t.Fatalf("Failed to dial tcp: %v", err)
+			}
+			defer tcpConn.Close()
+
+			// Write the HTTP/2 connection preface and the initial settings frame.
+			if _, err := tcpConn.Write([]byte("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n")); err != nil {
+				t.Fatalf("Failed to write preface: %v", err)
+			}
+			framer := http2.NewFramer(tcpConn, tcpConn)
+			if err := framer.WriteSettings(); err != nil {
+				t.Fatalf("Failed to write settings: %v", err)
+			}
+
+			// Encode and write the HEADERS frame.
+			var headerBuf bytes.Buffer
+			enc := hpack.NewEncoder(&headerBuf)
+			writeHeader := func(name, value string) {
+				enc.WriteField(hpack.HeaderField{Name: name, Value: value})
+			}
+			writeHeader(":method", "POST")
+			writeHeader(":scheme", "http")
+			writeHeader(":authority", ss.Address)
+			writeHeader(":path", tc.path)
+			writeHeader("content-type", "application/grpc")
+			writeHeader("te", "trailers")
+			if err := framer.WriteHeaders(http2.HeadersFrameParam{
+				StreamID:      1,
+				BlockFragment: headerBuf.Bytes(),
+				EndStream:     false,
+				EndHeaders:    true,
+			}); err != nil {
+				t.Fatalf("Failed to write headers: %v", err)
+			}
+
+			// Send a small gRPC-encoded data frame (0 length).
+			if err := framer.WriteData(1, true, []byte{0, 0, 0, 0, 0}); err != nil {
+				t.Fatalf("Failed to write data: %v", err)
+			}
+
+			// Read responses and look for grpc-status.
+			gotStatus := ""
+			dec := hpack.NewDecoder(4096, func(f hpack.HeaderField) {
+				if f.Name == "grpc-status" {
+					gotStatus = f.Value
+				}
+			})
+			done := make(chan struct{})
+			go func() {
+				defer close(done)
+				for {
+					frame, err := framer.ReadFrame()
+					if err != nil {
+						return
+					}
+					if headers, ok := frame.(*http2.HeadersFrame); ok {
+						if _, err := dec.Write(headers.HeaderBlockFragment()); err != nil {
+							return
+						}
+						if headers.StreamEnded() {
+							return
+						}
+					}
+				}
+			}()
+
+			select {
+			case <-done:
+			case <-ctx.Done():
+				t.Fatalf("Timed out waiting for response")
+			}
+
+			if gotStatus != tc.wantStatus {
+				t.Errorf("Got grpc-status %v, want %v", gotStatus, tc.wantStatus)
+			}
+		})
+	}
+}
-- 
2.53.0

openSUSE Build Service is sponsored by