diff --git a/context.go b/context.go index 5716318e1f..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,15 +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 - } - c.Render(code, render.JsonpJSON{Callback: callback, Data: obj}) +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 + // 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 { + c.JSON(http.StatusBadRequest, H{"error": "Invalid callback function name"}) + return + } + + // 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