From 65f7eb0fbae7f6efb1eecb82c6c15b101594ccde Mon Sep 17 00:00:00 2001 From: Jonathan Giannuzzi Date: Tue, 4 Mar 2025 13:55:43 +0000 Subject: [PATCH] rcserver: improve content-type check Some libraries use `application/json; charset=utf-8` as their `Content-Type`, which is valid. However we were not decoding the JSON body in that case, resulting in issues communicating with the rcserver. --- fs/rc/rcserver/rcserver.go | 23 ++++++++++-- fs/rc/rcserver/rcserver_test.go | 63 +++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/fs/rc/rcserver/rcserver.go b/fs/rc/rcserver/rcserver.go index 8cc666f3c..e6e82175b 100644 --- a/fs/rc/rcserver/rcserver.go +++ b/fs/rc/rcserver/rcserver.go @@ -209,8 +209,21 @@ func (s *Server) handlePost(w http.ResponseWriter, r *http.Request, path string) ctx := r.Context() contentType := r.Header.Get("Content-Type") + var ( + contentTypeMediaType string + contentTypeParams map[string]string + ) + if contentType != "" { + var err error + contentTypeMediaType, contentTypeParams, err = mime.ParseMediaType(contentType) + if err != nil { + writeError(path, nil, w, fmt.Errorf("failed to parse Content-Type: %w", err), http.StatusBadRequest) + return + } + } + values := r.URL.Query() - if contentType == "application/x-www-form-urlencoded" { + if contentTypeMediaType == "application/x-www-form-urlencoded" { // Parse the POST and URL parameters into r.Form, for others r.Form will be empty value err := r.ParseForm() if err != nil { @@ -229,7 +242,13 @@ func (s *Server) handlePost(w http.ResponseWriter, r *http.Request, path string) } // Parse a JSON blob from the input - if contentType == "application/json" { + if contentTypeMediaType == "application/json" { + // Check the charset is utf-8 or unset + if charset, ok := contentTypeParams["charset"]; ok && !strings.EqualFold(charset, "utf-8") { + writeError(path, in, w, fmt.Errorf("unsupported charset %q for JSON input", charset), http.StatusBadRequest) + return + } + err := json.NewDecoder(r.Body).Decode(&in) if err != nil { writeError(path, in, w, fmt.Errorf("failed to read input JSON: %w", err), http.StatusBadRequest) diff --git a/fs/rc/rcserver/rcserver_test.go b/fs/rc/rcserver/rcserver_test.go index d339d85b8..673aa4e69 100644 --- a/fs/rc/rcserver/rcserver_test.go +++ b/fs/rc/rcserver/rcserver_test.go @@ -429,6 +429,18 @@ func TestRC(t *testing.T) { "param1": "string", "param2": true } +`, + }, { + Name: "json-mixed-case-content-type", + URL: "rc/noop", + Method: "POST", + Body: `{ "param1":"string", "param2":true }`, + ContentType: "ApplicAtion/JsOn", + Status: http.StatusOK, + Expected: `{ + "param1": "string", + "param2": true +} `, }, { Name: "json-and-url-params", @@ -459,6 +471,44 @@ func TestRC(t *testing.T) { "path": "rc/noop", "status": 400 } +`, + }, { + Name: "json-charset", + URL: "rc/noop", + Method: "POST", + Body: `{ "param1":"string", "param2":true }`, + ContentType: "application/json; charset=utf-8", + Status: http.StatusOK, + Expected: `{ + "param1": "string", + "param2": true +} +`, + }, { + Name: "json-mixed-case-charset", + URL: "rc/noop", + Method: "POST", + Body: `{ "param1":"string", "param2":true }`, + ContentType: "aPPlication/jSoN; charset=UtF-8", + Status: http.StatusOK, + Expected: `{ + "param1": "string", + "param2": true +} +`, + }, { + Name: "json-bad-charset", + URL: "rc/noop", + Method: "POST", + Body: `{ "param1":"string", "param2":true }`, + ContentType: "application/json; charset=latin1", + Status: http.StatusBadRequest, + Expected: `{ + "error": "unsupported charset \"latin1\" for JSON input", + "input": {}, + "path": "rc/noop", + "status": 400 +} `, }, { Name: "form", @@ -498,6 +548,19 @@ func TestRC(t *testing.T) { "path": "rc/noop", "status": 400 } +`, + }, { + Name: "malformed-content-type", + URL: "rc/noop", + Method: "POST", + ContentType: "malformed/", + Status: http.StatusBadRequest, + Expected: `{ + "error": "failed to parse Content-Type: mime: expected token after slash", + "input": null, + "path": "rc/noop", + "status": 400 +} `, }} opt := newTestOpt()