feat: add SecureJSON func to prevent json hijacking

This commit is contained in:
Eason Lin 2017-07-08 01:21:30 +08:00
parent 0c3726b206
commit 75ed286c60
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})
}
// 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{}) {

View File

@ -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) {

25
gin.go
View File

@ -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)))

View File

@ -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)
}

View File

@ -14,6 +14,7 @@ type Render interface {
var (
_ Render = JSON{}
_ Render = IndentedJSON{}
_ Render = SecureJSON{}
_ Render = XML{}
_ Render = String{}
_ 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")
}
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