Repository: grpc-ecosystem/grpc-gateway (Go)

Issue:
The PathLengthFallback option picks fallback methods non-deterministically
## 🐛 Bug Report

When the `disablePathLengthFallback` option is enabled (the default behavior), the handler selection is non-deterministic, leading to unexpected (and potentially dangerous behavior, especially if DELETE methods are present in the handler.

The following code in `mux.go`, iterates over the map of handlers, but map iteration order is non-deterministic. The method assumes a fallback to POST, but if a DELETE method was defined for the same request path, it could be selected by mistake.
https://github.com/grpc-ecosystem/grpc-gateway/blob/836817217b6fe57bd95421b5fe7589c542e549d8/runtime/mux.go#L392-L441

This could be an important issue as a deletion handler could be called by mistake, potentially destroying data.


## To Reproduce

Say the following URIs and methods are valid methods of an API:
```
GET /resources/foo
DELETE /resources/foo
```
And someone by mistake runs a request to: `POST /resources/foo`.
If the fallback mechanism is enabled, it will fallback either to the GET or the DELETE handler (because if the POST request has a body the `isPathLengthFallback` method would return `true`). If the DELETE method is selected, a DELETE operation could be triggered by mistake.

## Expected behavior

The fallback mechanism should only fallback from GET to POST, but not to other HTTP verbs.

## Actual Behavior

The fallback mechanism fallbacks to any other method in a non-deterministic way.



Discussion:
Hi, thanks for raising this issue. That's definitely a bug (whoops!). Would you be interested in implementing a fix? At the very least we should make this deterministic, but you're right that it should probably also prioritize GETs.
Sure! I've raised a small PR changing the logic to only allow fallback for POST -> GET, as looking at the original feature discussions, I think that's the only case this code was targeting.



Patch:
diff --git a/runtime/mux.go b/runtime/mux.go
index 139bbbad49c..f451cb441f4 100644
--- a/runtime/mux.go
+++ b/runtime/mux.go
@@ -389,8 +389,12 @@ func (s *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	// lookup other methods to handle fallback from GET to POST and
-	// to determine if it is NotImplemented or NotFound.
+	// if no handler has found for the request, lookup for other methods
+	// to handle POST -> GET fallback if the request is subject to path
+	// length fallback.
+	// Note we are not eagerly checking the request here as we want to return the
+	// right HTTP status code, and we need to process the fallback candidates in
+	// order to do that.
 	for m, handlers := range s.handlers {
 		if m == r.Method {
 			continue
@@ -423,8 +427,11 @@ func (s *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 				}
 				continue
 			}
+
 			// X-HTTP-Method-Override is optional. Always allow fallback to POST.
-			if s.isPathLengthFallback(r) {
+			// Also, only consider POST -> GET fallbacks, and avoid falling back to
+			// potentially dangerous operations like DELETE.
+			if s.isPathLengthFallback(r) && m == http.MethodGet {
 				if err := r.ParseForm(); err != nil {
 					_, outboundMarshaler := MarshalerForRequest(s, r)
 					sterr := status.Error(codes.InvalidArgument, err.Error())


Test patch:
diff --git a/runtime/mux_test.go b/runtime/mux_test.go
index a4e709a2004..b284235c29e 100644
--- a/runtime/mux_test.go
+++ b/runtime/mux_test.go
@@ -206,6 +206,47 @@ func TestMuxServeHTTP(t *testing.T) {
 			respStatus:  http.StatusOK,
 			respContent: "GET /foo",
 		},
+		{
+			patterns: []stubPattern{
+				{
+					method: "GET",
+					ops:    []int{int(utilities.OpLitPush), 0},
+					pool:   []string{"foo"},
+				},
+			},
+			reqMethod: "POST",
+			reqPath:   "/foo",
+			headers: map[string]string{
+				"Content-Type": "application/x-www-form-urlencoded",
+			},
+			respStatus:  http.StatusOK,
+			respContent: "GET /foo",
+		},
+		{
+			patterns: []stubPattern{
+				{
+					method: "DELETE",
+					ops:    []int{int(utilities.OpLitPush), 0},
+					pool:   []string{"foo"},
+				},
+				{
+					method: "PUT",
+					ops:    []int{int(utilities.OpLitPush), 0},
+					pool:   []string{"foo"},
+				},
+				{
+					method: "PATCH",
+					ops:    []int{int(utilities.OpLitPush), 0},
+					pool:   []string{"foo"},
+				},
+			},
+			reqMethod: "POST",
+			reqPath:   "/foo",
+			headers: map[string]string{
+				"Content-Type": "application/x-www-form-urlencoded",
+			},
+			respStatus: http.StatusNotImplemented,
+		},
 		{
 			patterns: []stubPattern{
 				{