diff --git a/json/internal_types.mbt b/json/internal_types.mbt index 28fb52e2d..66fcfb8df 100644 --- a/json/internal_types.mbt +++ b/json/internal_types.mbt @@ -17,11 +17,17 @@ priv struct ParseContext { mut offset : Int input : String end_offset : Int + mut remaining_available_depth : Int } ///| -fn ParseContext::make(input : String) -> ParseContext { - { offset: 0, input, end_offset: input.length() } +fn ParseContext::make(input : String, max_nesting_depth : Int) -> ParseContext { + { + offset: 0, + input, + end_offset: input.length(), + remaining_available_depth: max_nesting_depth, + } } ///| diff --git a/json/json_test.mbt b/json/json_test.mbt index 3e09d6f1a..c81f63679 100644 --- a/json/json_test.mbt +++ b/json/json_test.mbt @@ -526,3 +526,28 @@ test "nested json" { }, }) } + +///| +test "call stack overflow" { + let depth = 50000 + let json_string = StringBuilder::new() + for _ in 0..=depth { + json_string.write_char('[') + } + for _ in 0..=depth { + json_string.write_char(']') + } + let _ = @json.parse(json_string.to_string()) catch { _ => null } + let depth = 1500 + let json_string = StringBuilder::new() + for _ in 0..=depth { + json_string.write_char('[') + } + for _ in 0..=depth { + json_string.write_char(']') + } + let _ = @json.parse(json_string.to_string(), max_nesting_depth=2000) catch { + _ => null + } + +} diff --git a/json/lex_misc.mbt b/json/lex_misc.mbt index ded622cd4..f053093a3 100644 --- a/json/lex_misc.mbt +++ b/json/lex_misc.mbt @@ -93,7 +93,7 @@ fn ParseContext::expect_ascii_char( ///| test "expect_char" { - let ctx = ParseContext::make("abc") + let ctx = ParseContext::make("abc", 1) ctx.expect_char('a') ctx.expect_char('b') ctx.expect_char('c') @@ -103,7 +103,7 @@ test "expect_char" { ///| test "expect_char with surrogate pair" { // "\uD83D\uDE00" // todo: shall we allow this? - let ctx = ParseContext::make("a\u{1F600}bc\u{1F600}c") + let ctx = ParseContext::make("a\u{1F600}bc\u{1F600}c", 1) ctx.expect_char('a') ctx.expect_char((0x1F600).unsafe_to_char()) ctx.expect_char('b') diff --git a/json/parse.mbt b/json/parse.mbt index 28aa0fddb..0d5cb95a2 100644 --- a/json/parse.mbt +++ b/json/parse.mbt @@ -23,8 +23,12 @@ pub fn valid(input : String) -> Bool { } ///| -pub fn parse(input : String) -> Json raise ParseError { - let ctx = ParseContext::make(input) +/// Parse a JSON input string into a Json value, with an optional maximum nesting depth (default is 1024) +pub fn parse( + input : String, + max_nesting_depth? : Int = 1024, +) -> Json raise ParseError { + let ctx = ParseContext::make(input, max_nesting_depth) let val = ctx.parse_value() ctx.lex_skip_whitespace() if ctx.offset >= ctx.end_offset { @@ -59,15 +63,25 @@ fn ParseContext::parse_value2( ///| fn ParseContext::parse_object(ctx : ParseContext) -> Json raise ParseError { + if ctx.remaining_available_depth <= 0 { + raise DepthLimitExceeded + } + ctx.remaining_available_depth -= 1 let map = Map::new() loop ctx.lex_property_name() { - RBrace => Json::object(map) + RBrace => { + ctx.remaining_available_depth += 1 + Json::object(map) + } String(name) => { ctx.lex_after_property_name() map[name] = ctx.parse_value() match ctx.lex_after_object_value() { Comma => continue ctx.lex_property_name2() - RBrace => Json::object(map) + RBrace => { + ctx.remaining_available_depth += 1 + Json::object(map) + } _ => abort("unreachable") } } @@ -77,15 +91,25 @@ fn ParseContext::parse_object(ctx : ParseContext) -> Json raise ParseError { ///| fn ParseContext::parse_array(ctx : ParseContext) -> Json raise ParseError { + if ctx.remaining_available_depth <= 0 { + raise DepthLimitExceeded + } + ctx.remaining_available_depth -= 1 let vec = [] loop ctx.lex_value(allow_rbracket=true) { - RBracket => Json::array(vec) + RBracket => { + ctx.remaining_available_depth += 1 + Json::array(vec) + } tok => { vec.push(ctx.parse_value2(tok)) let tok2 = ctx.lex_after_array_value() match tok2 { Comma => continue ctx.lex_value(allow_rbracket=false) - RBracket => Json::array(vec) + RBracket => { + ctx.remaining_available_depth += 1 + Json::array(vec) + } _ => abort("unreachable") } } diff --git a/json/pkg.generated.mbti b/json/pkg.generated.mbti index 5c328fd18..78c7701c5 100644 --- a/json/pkg.generated.mbti +++ b/json/pkg.generated.mbti @@ -7,7 +7,7 @@ fn[T : FromJson] from_json(Json, path? : JsonPath) -> T raise JsonDecodeError #callsite(autofill(args_loc, loc)) fn inspect(&ToJson, content? : Json, loc~ : SourceLoc, args_loc~ : ArgsLoc) -> Unit raise InspectError -fn parse(String) -> Json raise ParseError +fn parse(String, max_nesting_depth? : Int) -> Json raise ParseError fn valid(String) -> Bool @@ -22,6 +22,7 @@ pub(all) suberror ParseError { InvalidEof InvalidNumber(Position, String) InvalidIdentEscape(Position) + DepthLimitExceeded } impl Eq for ParseError impl Show for ParseError diff --git a/json/types.mbt b/json/types.mbt index f387b8296..3a50c4e59 100644 --- a/json/types.mbt +++ b/json/types.mbt @@ -24,6 +24,7 @@ pub(all) suberror ParseError { InvalidEof InvalidNumber(Position, String) InvalidIdentEscape(Position) + DepthLimitExceeded } derive(Eq, ToJson) ///| @@ -52,6 +53,10 @@ pub impl Show for ParseError with output(self, logger) { ..write_object(line) ..write_string(", column ") ..write_object(column) + DepthLimitExceeded => + logger.write_string( + "Depth limit exceeded, please increase the max_nesting_depth parameter", + ) } }