From 961513d2c1780d960598a3016272283b213a401d Mon Sep 17 00:00:00 2001 From: KDreynolds Date: Sat, 6 May 2023 23:45:05 -0600 Subject: [PATCH 1/2] added typechecking function --- context.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/context.go b/context.go index 5716318e1f..1c44f9de4a 100644 --- a/context.go +++ b/context.go @@ -962,6 +962,16 @@ func (c *Context) JSONP(code int, obj any) { c.Render(code, render.JSON{Data: obj}) return } + + // Add type checking for the callback function name + callbackPattern := `^[\p{L}\p{N}_]+$` // Unicode-aware pattern for alphanumeric characters and underscores + isValidCallback := regexp.MustCompile(callbackPattern).MatchString(callback) + if !isValidCallback { + // Handle the invalid callback function name, e.g., return an error or set a default callback function name + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid callback function name"}) + return + } + c.Render(code, render.JsonpJSON{Callback: callback, Data: obj}) } From c716cb94b7c0c6ce92e25fb1a0ea88d6a5e72bc4 Mon Sep 17 00:00:00 2001 From: KDreynolds Date: Sun, 7 May 2023 22:46:01 -0600 Subject: [PATCH 2/2] debugged function actually working, passed tests --- context.go | 55 ++++++++++++++++++++++++++++++++++++++----------- context_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++++ docs/doc.md | 6 ++++-- 3 files changed, 97 insertions(+), 14 deletions(-) diff --git a/context.go b/context.go index 1c44f9de4a..135d8eee84 100644 --- a/context.go +++ b/context.go @@ -5,6 +5,7 @@ package gin import ( + "encoding/json" "errors" "io" "log" @@ -15,6 +16,7 @@ import ( "net/url" "os" "path/filepath" + "regexp" "strings" "sync" "time" @@ -956,25 +958,54 @@ func (c *Context) SecureJSON(code int, obj any) { // JSONP serializes the given struct as JSON into the response body. // It adds padding to response body to request data from a server residing in a different domain than the client. // It also sets the Content-Type as "application/javascript". -func (c *Context) JSONP(code int, obj any) { - callback := c.DefaultQuery("callback", "") - if callback == "" { - c.Render(code, render.JSON{Data: obj}) - return - } +func (c *Context) JSONP(code int, obj interface{}) { + // Get the callback query parameter from the request or use an empty string as the default value + callback := c.DefaultQuery("callback", "") + + // If the callback query parameter is empty, respond with a JSON object + if callback == "" { + c.Render(code, render.JSON{Data: obj}) + return + } - // Add type checking for the callback function name - callbackPattern := `^[\p{L}\p{N}_]+$` // Unicode-aware pattern for alphanumeric characters and underscores + // Add type checking for the callback function name + // Use a Unicode-aware pattern for alphanumeric characters and underscores + callbackPattern := `^[\p{L}\p{N}_]+$` isValidCallback := regexp.MustCompile(callbackPattern).MatchString(callback) + + // If the callback function name is not valid, respond with an error message if !isValidCallback { - // Handle the invalid callback function name, e.g., return an error or set a default callback function name - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid callback function name"}) + c.JSON(http.StatusBadRequest, H{"error": "Invalid callback function name"}) return } - - c.Render(code, render.JsonpJSON{Callback: callback, Data: obj}) + + // Convert the input object to a slice of H (map[string]interface{}) values + var data []H + if d, ok := obj.([]H); ok { + data = d + } else if d, ok := obj.(H); ok { + data = []H{d} + } else { + data = []H{{"message": obj}} + } + + // Convert the H slice to a slice of empty interface values + var anyData []interface{} + for _, item := range data { + anyData = append(anyData, item) + } + + // Marshal the anyData slice to a JSON string + jsonString, _ := json.Marshal(anyData) + + // Respond with a JavaScript callback function call that includes the JSON data + c.Render(code, render.String{Format: "/**/ typeof " + callback + " === 'function' && " + callback + "(%s);", Data: []interface{}{string(jsonString)}}) } + + + + // JSON serializes the given struct as JSON into the response body. // It also sets the Content-Type as "application/json". func (c *Context) JSON(code int, obj any) { diff --git a/context_test.go b/context_test.go index 1dec902c69..ce7047d49f 100644 --- a/context_test.go +++ b/context_test.go @@ -2413,3 +2413,53 @@ func TestInterceptedHeader(t *testing.T) { assert.Equal(t, "", w.Result().Header.Get("X-Test")) assert.Equal(t, "present", w.Result().Header.Get("X-Test-2")) } + + +func TestJSONPCallbackTypeChecking(t *testing.T) { + router := New() + router.GET("/jsonp", func(c *Context) { + c.JSONP(http.StatusOK, H{"message": "success"}) + }) + testCases := []struct { + callback string + expected string + statusCode int + description string + }{ + { + callback: "validCallback", + expected: `/**/ typeof validCallback === 'function' && validCallback([{"message":"success"}]);`, + statusCode: http.StatusOK, + description: "Valid callback function name", + }, + + { + callback: url.QueryEscape("invalidCallback();"), + expected: "{\"error\":\"Invalid callback function name\"}", + statusCode: http.StatusBadRequest, + description: "Invalid callback function name", + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + req, _ := http.NewRequest("GET", "/jsonp?callback="+tc.callback, nil) + resp := httptest.NewRecorder() + + router.ServeHTTP(resp, req) + + if resp.Code != tc.statusCode { + t.Errorf("Expected status code %d, got %d", tc.statusCode, resp.Code) + } + + actualBody := resp.Body.String() + expectedBody := tc.expected + if actualBody != expectedBody { + t.Errorf("Expected response body %q, got %q", expectedBody, actualBody) + } + }) + } +} + + + diff --git a/docs/doc.md b/docs/doc.md index e48c2ba183..5edfbfda03 100644 --- a/docs/doc.md +++ b/docs/doc.md @@ -1071,7 +1071,9 @@ func main() { #### JSONP -Using JSONP to request data from a server in a different domain. Add callback to response body if the query parameter callback exists. +Using JSONP to request data from a server in a different domain. Add callback to response body if the query parameter `callback` exists and contains a valid callback function name. Valid callback function names consist of alphanumeric characters and underscores. + +Note: To enhance security, Gin performs type checking on the JSONP callback function names. Only alphanumeric characters and underscores are allowed in the callback function names. If an invalid callback function name is provided, Gin will return an error. This type checking mechanism helps prevent attackers from exploiting the JSONP endpoint to bypass content security headers and execute malicious scripts. ```go func main() { @@ -1093,7 +1095,7 @@ func main() { // client // curl http://127.0.0.1:8080/JSONP?callback=x } -``` + #### AsciiJSON