Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions docs/docs/mapping/customizing_your_gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
16 changes: 15 additions & 1 deletion runtime/mux.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ type ServeMux struct {
streamErrorHandler StreamErrorHandlerFunc
routingErrorHandler RoutingErrorHandlerFunc
disablePathLengthFallback bool
disableHTTPMethodOverride bool
unescapingMode UnescapingMode
writeContentLength bool
disableChunkedEncoding bool
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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())
Expand Down
36 changes: 36 additions & 0 deletions runtime/mux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading