diff --git a/docs/docs/mapping/customizing_your_gateway.md b/docs/docs/mapping/customizing_your_gateway.md index 66afee448b4..c9afd0f7006 100644 --- a/docs/docs/mapping/customizing_your_gateway.md +++ b/docs/docs/mapping/customizing_your_gateway.md @@ -492,3 +492,33 @@ mux := runtime.NewServeMux( runtime.WithRoutingErrorHandler(handleRoutingError), ) ``` + +## Disabling X-HTTP-Method-Override + +By default, the gRPC-Gateway allows clients to send a `POST` request with an +`X-HTTP-Method-Override` header to override the HTTP method. For example, a +`POST` request with `X-HTTP-Method-Override: GET` will be routed as if it were +a `GET` request. This is part of the path length fallback behavior, which +allows HTML forms (which only support `GET` and `POST`) to call other methods. + +This can lead to HTTP method confusion when your gateway sits behind a Web +Application Firewall (WAF) or reverse proxy that enforces method-based access +controls. For example, if a WAF is configured to only allow `POST` requests to +a particular endpoint, a client could send a `POST` with +`X-HTTP-Method-Override: DELETE` and the gateway would route the request to the +`DELETE` handler, bypassing the WAF's intended restrictions. The WAF sees a +`POST` request, but the gateway processes it as a `DELETE`. + +To disable the `X-HTTP-Method-Override` header handling, use the +`WithDisableHTTPMethodOverride` option: + +```go +mux := runtime.NewServeMux( + runtime.WithDisableHTTPMethodOverride(), +) +``` + +This disables only the method override header. The path length fallback (routing +a `POST` with `Content-Type: application/x-www-form-urlencoded` to a matching +`GET` handler) remains available unless separately disabled with +`WithDisablePathLengthFallback`. diff --git a/runtime/mux.go b/runtime/mux.go index 4e684c7de6c..6d483e8cccd 100644 --- a/runtime/mux.go +++ b/runtime/mux.go @@ -71,6 +71,7 @@ type ServeMux struct { streamErrorHandler StreamErrorHandlerFunc routingErrorHandler RoutingErrorHandlerFunc disablePathLengthFallback bool + disableHTTPMethodOverride bool unescapingMode UnescapingMode writeContentLength bool disableChunkedEncoding bool @@ -271,6 +272,19 @@ func WithDisablePathLengthFallback() ServeMuxOption { } } +// WithDisableHTTPMethodOverride returns a ServeMuxOption that disables the +// X-HTTP-Method-Override header handling. +// +// When this option is used, the mux will no longer allow POST requests with +// the X-HTTP-Method-Override header to override the HTTP method. The path +// length fallback (POST with application/x-www-form-urlencoded falling back +// to a matching GET handler) is not affected by this option. +func WithDisableHTTPMethodOverride() ServeMuxOption { + return func(serveMux *ServeMux) { + serveMux.disableHTTPMethodOverride = true + } +} + // WithWriteContentLength returns a ServeMuxOption to enable writing content length on non-streaming responses func WithWriteContentLength() ServeMuxOption { return func(serveMux *ServeMux) { @@ -405,7 +419,7 @@ func (s *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { path = r.URL.RawPath } - if override := r.Header.Get("X-HTTP-Method-Override"); override != "" && s.isPathLengthFallback(r) { + if override := r.Header.Get("X-HTTP-Method-Override"); override != "" && !s.disableHTTPMethodOverride && s.isPathLengthFallback(r) { if err := r.ParseForm(); err != nil { _, outboundMarshaler := MarshalerForRequest(s, r) sterr := status.Error(codes.InvalidArgument, err.Error()) diff --git a/runtime/mux_test.go b/runtime/mux_test.go index c3df8109dd2..1efd21224d4 100644 --- a/runtime/mux_test.go +++ b/runtime/mux_test.go @@ -930,3 +930,39 @@ func TestServeMux_InjectPattern(t *testing.T) { t.Errorf("request not processed") } } + +func TestServeHTTP_WithDisableHTTPMethodOverride(t *testing.T) { + // When WithDisableHTTPMethodOverride is set, X-HTTP-Method-Override + // header should be ignored and the request should match the POST + // handler directly. + mux := runtime.NewServeMux(runtime.WithDisableHTTPMethodOverride()) + + pat, err := runtime.NewPattern(1, []int{int(utilities.OpLitPush), 0}, []string{"foo"}, "") + if err != nil { + t.Fatalf("runtime.NewPattern failed: %v", err) + } + mux.Handle("GET", pat, func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) { + _, _ = fmt.Fprintf(w, "GET /foo") + }) + + postPat, err := runtime.NewPattern(1, []int{int(utilities.OpLitPush), 0}, []string{"foo"}, "") + if err != nil { + t.Fatalf("runtime.NewPattern failed: %v", err) + } + mux.Handle("POST", postPat, func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) { + _, _ = fmt.Fprintf(w, "POST /foo") + }) + + r := httptest.NewRequest("POST", "https://host.example/foo", bytes.NewReader(nil)) + r.Header.Set("Content-Type", "application/x-www-form-urlencoded") + r.Header.Set("X-HTTP-Method-Override", "GET") + w := httptest.NewRecorder() + mux.ServeHTTP(w, r) + + if got, want := w.Code, http.StatusOK; got != want { + t.Errorf("w.Code = %d; want %d", got, want) + } + if got, want := w.Body.String(), "POST /foo"; got != want { + t.Errorf("w.Body = %q; want %q", got, want) + } +}