From 75ed286c6038b2a366ecf033a79d31764eeeb041 Mon Sep 17 00:00:00 2001 From: Eason Lin Date: Sat, 8 Jul 2017 01:21:30 +0800 Subject: [PATCH] feat: add SecureJSON func to prevent json hijacking --- context.go | 7 +++++++ context_test.go | 26 ++++++++++++++++++++++++++ gin.go | 25 ++++++++++++++++--------- render/json.go | 26 ++++++++++++++++++++++++++ render/render.go | 1 + render/render_test.go | 25 +++++++++++++++++++++++++ 6 files changed, 101 insertions(+), 9 deletions(-) diff --git a/context.go b/context.go index 604dc4ef..acf2cd23 100644 --- a/context.go +++ b/context.go @@ -616,6 +616,13 @@ func (c *Context) IndentedJSON(code int, obj interface{}) { c.Render(code, render.IndentedJSON{Data: obj}) } +// SecureJSON serializes the given struct as Secure JSON into the response body. +// Default prepends "while(1)," to response body if the given struct is array values. +// It also sets the Content-Type as "application/json". +func (c *Context) SecureJSON(code int, obj interface{}) { + c.Render(code, render.SecureJSON{Prefix: c.engine.secureJsonPrefix, Data: obj}) +} + // 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 interface{}) { diff --git a/context_test.go b/context_test.go index fe31f09a..96e9f08e 100644 --- a/context_test.go +++ b/context_test.go @@ -598,6 +598,32 @@ func TestContextRenderNoContentIndentedJSON(t *testing.T) { assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8") } +// Tests that the response is serialized as Secure JSON +// and Content-Type is set to application/json +func TestContextRenderSecureJSON(t *testing.T) { + w := httptest.NewRecorder() + c, router := CreateTestContext(w) + + router.SecureJsonPrefix("&&&START&&&") + c.SecureJSON(201, []string{"foo", "bar"}) + + assert.Equal(t, w.Code, 201) + assert.Equal(t, w.Body.String(), "&&&START&&&[\"foo\",\"bar\"]") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8") +} + +// Tests that no Custom JSON is rendered if code is 204 +func TestContextRenderNoContentSecureJSON(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.SecureJSON(204, []string{"foo", "bar"}) + + assert.Equal(t, 204, w.Code) + assert.Equal(t, "", w.Body.String()) + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8") +} + // Tests that the response executes the templates // and responds with Content-Type set to text/html func TestContextRenderHTML(t *testing.T) { diff --git a/gin.go b/gin.go index 3cac936c..8388ac7b 100644 --- a/gin.go +++ b/gin.go @@ -44,15 +44,16 @@ type RoutesInfo []RouteInfo // Create an instance of Engine, by using New() or Default() type Engine struct { RouterGroup - delims render.Delims - HTMLRender render.HTMLRender - FuncMap template.FuncMap - allNoRoute HandlersChain - allNoMethod HandlersChain - noRoute HandlersChain - noMethod HandlersChain - pool sync.Pool - trees methodTrees + delims render.Delims + secureJsonPrefix string + HTMLRender render.HTMLRender + FuncMap template.FuncMap + allNoRoute HandlersChain + allNoMethod HandlersChain + noRoute HandlersChain + noMethod HandlersChain + pool sync.Pool + trees methodTrees // Enables automatic redirection if the current route can't be matched but a // handler for the path with (without) the trailing slash exists. @@ -121,6 +122,7 @@ func New() *Engine { UnescapePathValues: true, trees: make(methodTrees, 0, 9), delims: render.Delims{"{{", "}}"}, + secureJsonPrefix: "while(1);", } engine.RouterGroup.engine = engine engine.pool.New = func() interface{} { @@ -145,6 +147,11 @@ func (engine *Engine) Delims(left, right string) *Engine { return engine } +func (engine *Engine) SecureJsonPrefix(prefix string) *Engine { + engine.secureJsonPrefix = prefix + return engine +} + func (engine *Engine) LoadHTMLGlob(pattern string) { if IsDebugging() { debugPrintLoadTemplate(template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseGlob(pattern))) diff --git a/render/json.go b/render/json.go index 4e3b66b2..8b64f533 100644 --- a/render/json.go +++ b/render/json.go @@ -5,6 +5,7 @@ package render import ( + "bytes" "encoding/json" "net/http" ) @@ -17,6 +18,13 @@ type IndentedJSON struct { Data interface{} } +type SecureJSON struct { + Prefix string + Data interface{} +} + +type SecureJSONPrefix string + var jsonContentType = []string{"application/json; charset=utf-8"} func (r JSON) Render(w http.ResponseWriter) (err error) { @@ -53,3 +61,21 @@ func (r IndentedJSON) Render(w http.ResponseWriter) error { func (r IndentedJSON) WriteContentType(w http.ResponseWriter) { writeContentType(w, jsonContentType) } + +func (r SecureJSON) Render(w http.ResponseWriter) error { + r.WriteContentType(w) + jsonBytes, err := json.Marshal(r.Data) + if err != nil { + return err + } + // if the jsonBytes is array values + if bytes.HasPrefix(jsonBytes, []byte("[")) && bytes.HasSuffix(jsonBytes, []byte("]")) { + w.Write([]byte(r.Prefix)) + } + w.Write(jsonBytes) + return nil +} + +func (r SecureJSON) WriteContentType(w http.ResponseWriter) { + writeContentType(w, jsonContentType) +} diff --git a/render/render.go b/render/render.go index 46291421..620b0d87 100644 --- a/render/render.go +++ b/render/render.go @@ -14,6 +14,7 @@ type Render interface { var ( _ Render = JSON{} _ Render = IndentedJSON{} + _ Render = SecureJSON{} _ Render = XML{} _ Render = String{} _ Render = Redirect{} diff --git a/render/render_test.go b/render/render_test.go index c48235c3..e4df5b6d 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -66,6 +66,31 @@ func TestRenderIndentedJSON(t *testing.T) { assert.Equal(t, w.Header().Get("Content-Type"), "application/json; charset=utf-8") } +func TestRenderSecureJSON(t *testing.T) { + w1 := httptest.NewRecorder() + data := map[string]interface{}{ + "foo": "bar", + } + + err1 := (SecureJSON{"while(1);", data}).Render(w1) + + assert.NoError(t, err1) + assert.Equal(t, "{\"foo\":\"bar\"}", w1.Body.String()) + assert.Equal(t, "application/json; charset=utf-8", w1.Header().Get("Content-Type")) + + w2 := httptest.NewRecorder() + datas := []map[string]interface{}{{ + "foo": "bar", + }, { + "bar": "foo", + }} + + err2 := (SecureJSON{"while(1);", datas}).Render(w2) + assert.NoError(t, err2) + assert.Equal(t, "while(1);[{\"foo\":\"bar\"},{\"bar\":\"foo\"}]", w2.Body.String()) + assert.Equal(t, "application/json; charset=utf-8", w2.Header().Get("Content-Type")) +} + type xmlmap map[string]interface{} // Allows type H to be used with xml.Marshal