Преглед на файлове

Merge pull request #987 from easonlin404/secure-json

feat(render): add SecureJSON func to prevent json hijacking
Javier Provecho Fernandez преди 8 години
родител
ревизия
c4249f923f
променени са 6 файла, в които са добавени 101 реда и са изтрити 9 реда
  1. 7 0
      context.go
  2. 26 0
      context_test.go
  3. 16 9
      gin.go
  4. 26 0
      render/json.go
  5. 1 0
      render/render.go
  6. 25 0
      render/render_test.go

+ 7 - 0
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{}) {

+ 26 - 0
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) {

+ 16 - 9
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)))

+ 26 - 0
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)
+}

+ 1 - 0
render/render.go

@@ -14,6 +14,7 @@ type Render interface {
 var (
 	_ Render     = JSON{}
 	_ Render     = IndentedJSON{}
+	_ Render     = SecureJSON{}
 	_ Render     = XML{}
 	_ Render     = String{}
 	_ Render     = Redirect{}

+ 25 - 0
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