diff --git a/errno/errcode.go b/errno/errcode.go index febcff92fa90f..237be0fc14d03 100644 --- a/errno/errcode.go +++ b/errno/errcode.go @@ -850,6 +850,7 @@ const ( ErrInvalidJSONPathWildcard = 3149 ErrInvalidJSONContainsPathType = 3150 ErrJSONUsedAsKey = 3152 + ErrJSONDocumentTooDeep = 3157 ErrJSONDocumentNULLKey = 3158 ErrSecureTransportRequired = 3159 ErrBadUser = 3162 diff --git a/errno/errname.go b/errno/errname.go index 3ff6d6a36f53a..913473ad4c3f5 100644 --- a/errno/errname.go +++ b/errno/errname.go @@ -852,6 +852,7 @@ var MySQLErrName = map[uint16]*mysql.ErrMessage{ ErrInvalidJSONPathWildcard: mysql.Message("In this situation, path expressions may not contain the * and ** tokens.", nil), ErrInvalidJSONContainsPathType: mysql.Message("The second argument can only be either 'one' or 'all'.", nil), ErrJSONUsedAsKey: mysql.Message("JSON column '%-.192s' cannot be used in key specification.", nil), + ErrJSONDocumentTooDeep: mysql.Message("The JSON document exceeds the maximum depth.", nil), ErrJSONDocumentNULLKey: mysql.Message("JSON documents may not contain NULL member names.", nil), ErrSecureTransportRequired: mysql.Message("Connections using insecure transport are prohibited while --require_secure_transport=ON.", nil), ErrBadUser: mysql.Message("User %s does not exist.", nil), diff --git a/errors.toml b/errors.toml index 06b4ad99e3a53..e498e44d6b26e 100644 --- a/errors.toml +++ b/errors.toml @@ -1621,6 +1621,11 @@ error = ''' The second argument can only be either 'one' or 'all'. ''' +["json:3157"] +error = ''' +The JSON document exceeds the maximum depth. +''' + ["json:3158"] error = ''' JSON documents may not contain NULL member names. diff --git a/executor/aggfuncs/func_json_arrayagg.go b/executor/aggfuncs/func_json_arrayagg.go index 978da8b5e3411..dfabf88e3620f 100644 --- a/executor/aggfuncs/func_json_arrayagg.go +++ b/executor/aggfuncs/func_json_arrayagg.go @@ -54,7 +54,11 @@ func (e *jsonArrayagg) AppendFinalResult2Chunk(sctx sessionctx.Context, pr Parti return nil } - chk.AppendJSON(e.ordinal, types.CreateBinaryJSON(p.entries)) + json, err := types.CreateBinaryJSONWithCheck(p.entries) + if err != nil { + return errors.Trace(err) + } + chk.AppendJSON(e.ordinal, json) return nil } diff --git a/executor/aggfuncs/func_json_objectagg.go b/executor/aggfuncs/func_json_objectagg.go index c7fb0e47816cd..361534db08c02 100644 --- a/executor/aggfuncs/func_json_objectagg.go +++ b/executor/aggfuncs/func_json_objectagg.go @@ -61,7 +61,11 @@ func (e *jsonObjectAgg) AppendFinalResult2Chunk(sctx sessionctx.Context, pr Part return nil } - chk.AppendJSON(e.ordinal, types.CreateBinaryJSON(p.entries)) + bj, err := types.CreateBinaryJSONWithCheck(p.entries) + if err != nil { + return errors.Trace(err) + } + chk.AppendJSON(e.ordinal, bj) return nil } diff --git a/expression/builtin_json.go b/expression/builtin_json.go index fd1ae8644eef1..c85e5b6ae0d25 100644 --- a/expression/builtin_json.go +++ b/expression/builtin_json.go @@ -554,7 +554,11 @@ func (b *builtinJSONObjectSig) evalJSON(row chunk.Row) (res types.BinaryJSON, is jsons[key] = value } } - return types.CreateBinaryJSON(jsons), false, nil + bj, err := types.CreateBinaryJSONWithCheck(jsons) + if err != nil { + return res, true, err + } + return bj, false, nil } type jsonArrayFunctionClass struct { @@ -603,7 +607,11 @@ func (b *builtinJSONArraySig) evalJSON(row chunk.Row) (res types.BinaryJSON, isN } jsons = append(jsons, j) } - return types.CreateBinaryJSON(jsons), false, nil + bj, err := types.CreateBinaryJSONWithCheck(jsons) + if err != nil { + return res, true, err + } + return bj, false, nil } type jsonContainsPathFunctionClass struct { @@ -976,7 +984,10 @@ func (b *builtinJSONArrayAppendSig) appendJSONArray(res types.BinaryJSON, p stri // res.Extract will return a json object instead of an array if there is an object at path pathExpr. // JSON_ARRAY_APPEND({"a": "b"}, "$", {"b": "c"}) => [{"a": "b"}, {"b", "c"}] // We should wrap them to a single array first. - obj = types.CreateBinaryJSON([]interface{}{obj}) + obj, err = types.CreateBinaryJSONWithCheck([]interface{}{obj}) + if err != nil { + return res, true, err + } } obj = types.MergeBinaryJSON([]types.BinaryJSON{obj, v}) diff --git a/expression/builtin_json_vec.go b/expression/builtin_json_vec.go index ae89ed66267ff..7c6d9e5f519f8 100644 --- a/expression/builtin_json_vec.go +++ b/expression/builtin_json_vec.go @@ -238,7 +238,11 @@ func (b *builtinJSONArraySig) vecEvalJSON(input *chunk.Chunk, result *chunk.Colu } result.ReserveJSON(nr) for i := 0; i < nr; i++ { - result.AppendJSON(types.CreateBinaryJSON(jsons[i])) + bj, err := types.CreateBinaryJSONWithCheck(jsons[i]) + if err != nil { + return err + } + result.AppendJSON(bj) } return nil } @@ -537,7 +541,11 @@ func (b *builtinJSONObjectSig) vecEvalJSON(input *chunk.Chunk, result *chunk.Col } for i := 0; i < nr; i++ { - result.AppendJSON(types.CreateBinaryJSON(jsons[i])) + bj, err := types.CreateBinaryJSONWithCheck(jsons[i]) + if err != nil { + return err + } + result.AppendJSON(bj) } return nil } diff --git a/expression/integration_test.go b/expression/integration_test.go index 379b08e35ffca..be9448a1e0048 100644 --- a/expression/integration_test.go +++ b/expression/integration_test.go @@ -7532,6 +7532,42 @@ func TestCastRealAsTime(t *testing.T) { " ")) } +func TestJSONDepth(t *testing.T) { + store := testkit.CreateMockStore(t) + tk := testkit.NewTestKit(t, store) + tk.MustExec("use test") + tk.MustExec("create table t(a JSON)") + tk.MustGetErrCode(`insert into t +with recursive c1 as (select cast(1 as signed) c, json_array(1) as a + union + select c + 1, json_array_insert(a, concat('$', repeat('[0]', c)), json_array(1)) + from c1 + where c < 101) +select a from c1 where c > 100;`, errno.ErrJSONDocumentTooDeep) + tk.MustExec(`insert into t +with recursive c1 as (select cast(1 as signed) c, json_array(1) as a + union + select c + 1, json_array_insert(a, concat('$', repeat('[0]', c)), json_array(1)) + from c1 + where c < 100) +select a from c1 where c > 99;`) + + err := tk.QueryToErr(`select json_array(a, 1) from t`) + require.Error(t, err) + // FIXME: mysql client shows the error. + //err = tk.QueryToErr(`select json_objectagg(1, a) from t;`) + //require.Error(t, err) + err = tk.QueryToErr(`select json_object(1, a) from t;`) + require.Error(t, err) + err = tk.QueryToErr(`select json_set(a, concat('$', repeat('[0]', 100)), json_array(json_array(3))) from t;`) + require.Error(t, err) + err = tk.QueryToErr(`select json_array_append(a, concat('$', repeat('[0]', 100)), 1) from t;`) + require.Error(t, err) + // FIXME: mysql client shows the error. + //err = tk.QueryToErr(`select json_arrayagg(a) from t;`) + //require.Error(t, err) +} + func TestCastJSONTimeDuration(t *testing.T) { store := testkit.CreateMockStore(t) tk := testkit.NewTestKit(t, store) diff --git a/types/json_binary.go b/types/json_binary.go index 22084a869480e..1b03c5678177d 100644 --- a/types/json_binary.go +++ b/types/json_binary.go @@ -120,6 +120,8 @@ import ( var jsonZero = CreateBinaryJSON(uint64(0)) +const maxJSONDepth = 100 + // BinaryJSON represents a binary encoded JSON object. // It can be randomly accessed without deserialization. type BinaryJSON struct { @@ -514,14 +516,12 @@ func (bj *BinaryJSON) UnmarshalJSON(data []byte) error { if err != nil { return errors.Trace(err) } - buf := make([]byte, 0, len(data)) - var typeCode JSONTypeCode - typeCode, buf, err = appendBinaryJSON(buf, in) + newBj, err := CreateBinaryJSONWithCheck(in) if err != nil { return errors.Trace(err) } - bj.TypeCode = typeCode - bj.Value = buf + bj.TypeCode = newBj.TypeCode + bj.Value = newBj.Value return nil } @@ -557,11 +557,25 @@ func (bj BinaryJSON) HashValue(buf []byte) []byte { // CreateBinaryJSON creates a BinaryJSON from interface. func CreateBinaryJSON(in interface{}) BinaryJSON { - typeCode, buf, err := appendBinaryJSON(nil, in) + bj, err := CreateBinaryJSONWithCheck(in) if err != nil { panic(err) } - return BinaryJSON{TypeCode: typeCode, Value: buf} + return bj +} + +// CreateBinaryJSONWithCheck creates a BinaryJSON from interface with error check. +func CreateBinaryJSONWithCheck(in interface{}) (BinaryJSON, error) { + typeCode, buf, err := appendBinaryJSON(nil, in) + if err != nil { + return BinaryJSON{}, err + } + bj := BinaryJSON{TypeCode: typeCode, Value: buf} + // GetElemDepth always returns +1. + if bj.GetElemDepth()-1 > maxJSONDepth { + return BinaryJSON{}, ErrJSONDocumentTooDeep + } + return bj, nil } func appendBinaryJSON(buf []byte, in interface{}) (JSONTypeCode, []byte, error) { diff --git a/types/json_binary_functions.go b/types/json_binary_functions.go index bcd61807c2e12..71e6e1854ff56 100644 --- a/types/json_binary_functions.go +++ b/types/json_binary_functions.go @@ -409,6 +409,9 @@ func (bj BinaryJSON) Modify(pathExprList []JSONPathExpression, values []BinaryJS return BinaryJSON{}, modifier.err } } + if bj.GetElemDepth()-1 > maxJSONDepth { + return bj, ErrJSONDocumentTooDeep + } return bj, nil } diff --git a/types/json_constants.go b/types/json_constants.go index 877c604d7cc8f..56ff6df1cd9f7 100644 --- a/types/json_constants.go +++ b/types/json_constants.go @@ -230,6 +230,8 @@ var ( ErrInvalidJSONContainsPathType = dbterror.ClassJSON.NewStd(mysql.ErrInvalidJSONContainsPathType) // ErrJSONDocumentNULLKey means that json's key is null ErrJSONDocumentNULLKey = dbterror.ClassJSON.NewStd(mysql.ErrJSONDocumentNULLKey) + // ErrJSONDocumentTooDeep means that json's depth is too deep. + ErrJSONDocumentTooDeep = dbterror.ClassJSON.NewStd(mysql.ErrJSONDocumentTooDeep) // ErrJSONObjectKeyTooLong means JSON object with key length >= 65536 which is not yet supported. ErrJSONObjectKeyTooLong = dbterror.ClassTypes.NewStdErr(mysql.ErrJSONObjectKeyTooLong, mysql.MySQLErrName[mysql.ErrJSONObjectKeyTooLong]) // ErrInvalidJSONPathArrayCell means invalid JSON path for an array cell.