Merge pull request #987 from easonlin404/secure-json

feat(render): add SecureJSON func to prevent json hijacking
This commit is contained in:
Javier Provecho Fernandez 2017-07-08 10:41:41 +02:00 committed by GitHub
commit c4249f923f
6 changed files with 101 additions and 9 deletions

View File

@ -616,6 +616,13 @@ func (c *Context) IndentedJSON(code int, obj interface{}) {
c.Render(code, render.IndentedJSON{Data: obj}) 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. // JSON serializes the given struct as JSON into the response body.
// It also sets the Content-Type as "application/json". // It also sets the Content-Type as "application/json".
func (c *Context) JSON(code int, obj interface{}) { func (c *Context) JSON(code int, obj interface{}) {

View File

@ -598,6 +598,32 @@ func TestContextRenderNoContentIndentedJSON(t *testing.T) {
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8") 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 // Tests that the response executes the templates
// and responds with Content-Type set to text/html // and responds with Content-Type set to text/html
func TestContextRenderHTML(t *testing.T) { func TestContextRenderHTML(t *testing.T) {

25
gin.go
View File

@ -44,15 +44,16 @@ type RoutesInfo []RouteInfo
// Create an instance of Engine, by using New() or Default() // Create an instance of Engine, by using New() or Default()
type Engine struct { type Engine struct {
RouterGroup RouterGroup
delims render.Delims delims render.Delims
HTMLRender render.HTMLRender secureJsonPrefix string
FuncMap template.FuncMap HTMLRender render.HTMLRender
allNoRoute HandlersChain FuncMap template.FuncMap
allNoMethod HandlersChain allNoRoute HandlersChain
noRoute HandlersChain allNoMethod HandlersChain
noMethod HandlersChain noRoute HandlersChain
pool sync.Pool noMethod HandlersChain
trees methodTrees pool sync.Pool
trees methodTrees
// Enables automatic redirection if the current route can't be matched but a // Enables automatic redirection if the current route can't be matched but a
// handler for the path with (without) the trailing slash exists. // handler for the path with (without) the trailing slash exists.
@ -121,6 +122,7 @@ func New() *Engine {
UnescapePathValues: true, UnescapePathValues: true,
trees: make(methodTrees, 0, 9), trees: make(methodTrees, 0, 9),
delims: render.Delims{"{{", "}}"}, delims: render.Delims{"{{", "}}"},
secureJsonPrefix: "while(1);",
} }
engine.RouterGroup.engine = engine engine.RouterGroup.engine = engine
engine.pool.New = func() interface{} { engine.pool.New = func() interface{} {
@ -145,6 +147,11 @@ func (engine *Engine) Delims(left, right string) *Engine {
return engine return engine
} }
func (engine *Engine) SecureJsonPrefix(prefix string) *Engine {
engine.secureJsonPrefix = prefix
return engine
}
func (engine *Engine) LoadHTMLGlob(pattern string) { func (engine *Engine) LoadHTMLGlob(pattern string) {
if IsDebugging() { if IsDebugging() {
debugPrintLoadTemplate(template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseGlob(pattern))) debugPrintLoadTemplate(template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseGlob(pattern)))

View File

@ -5,6 +5,7 @@
package render package render
import ( import (
"bytes"
"encoding/json" "encoding/json"
"net/http" "net/http"
) )
@ -17,6 +18,13 @@ type IndentedJSON struct {
Data interface{} Data interface{}
} }
type SecureJSON struct {
Prefix string
Data interface{}
}
type SecureJSONPrefix string
var jsonContentType = []string{"application/json; charset=utf-8"} var jsonContentType = []string{"application/json; charset=utf-8"}
func (r JSON) Render(w http.ResponseWriter) (err error) { 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) { func (r IndentedJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonContentType) 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)
}

View File

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

View File

@ -66,6 +66,31 @@ func TestRenderIndentedJSON(t *testing.T) {
assert.Equal(t, w.Header().Get("Content-Type"), "application/json; charset=utf-8") 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{} type xmlmap map[string]interface{}
// Allows type H to be used with xml.Marshal // Allows type H to be used with xml.Marshal