From: UrielCh Date: Sun, 5 Mar 2023 13:01:10 +0200 Subject: CVE-2023-1370: stack overflow due to excessive recursion MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: 8bit When reaching a ‘[‘ or ‘{‘ character in the JSON input, the code parses an array or an object respectively. It was discovered that the code does not have any limit to the nesting of such arrays or objects. Since the parsing of nested arrays and objects is done recursively, nesting too many of them can cause a stack exhaustion (stack overflow) and crash the software. origin: https://github.com/netplex/json-smart-v2/commit/5b3205d051952d3100aa0db1535f6ba6226bd87a.patch bug: https://research.jfrog.com/vulnerabilities/stack-exhaustion-in-json-smart-leads-to-denial-of-service-when-parsing-malformed-json-xray-427633/ bug-debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1033474 --- .../net/minidev/json/parser/JSONParserBase.java | 17 +++++++++++++- .../net/minidev/json/parser/ParseException.java | 9 +++++++- .../java/net/minidev/json/test/TestOverflow.java | 27 ++++++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 json-smart/src/test/java/net/minidev/json/test/TestOverflow.java diff --git a/json-smart/src/main/java/net/minidev/json/parser/JSONParserBase.java b/json-smart/src/main/java/net/minidev/json/parser/JSONParserBase.java index 96d6bb6..f65b8c5 100644 --- a/json-smart/src/main/java/net/minidev/json/parser/JSONParserBase.java +++ b/json-smart/src/main/java/net/minidev/json/parser/JSONParserBase.java @@ -20,6 +20,7 @@ import static net.minidev.json.parser.ParseException.ERROR_UNEXPECTED_EOF; import static net.minidev.json.parser.ParseException.ERROR_UNEXPECTED_LEADING_0; import static net.minidev.json.parser.ParseException.ERROR_UNEXPECTED_TOKEN; import static net.minidev.json.parser.ParseException.ERROR_UNEXPECTED_UNICODE; +import static net.minidev.json.parser.ParseException.ERROR_UNEXPECTED_JSON_DEPTH; import java.io.IOException; import java.math.BigDecimal; @@ -39,6 +40,12 @@ import net.minidev.json.writer.JsonReaderI; */ abstract class JSONParserBase { protected char c; + /** + * hard coded maximal depth for JSON parsing + */ + public final static int MAX_DEPTH = 400; + protected int depth = 0; + JsonReader base; public final static byte EOI = 0x1A; protected static final char MAX_STOP = 126; // '}' -> 125 @@ -232,9 +239,12 @@ abstract class JSONParserBase { abstract protected void read() throws IOException; protected T readArray(JsonReaderI mapper) throws ParseException, IOException { - Object current = mapper.createArray(); if (c != '[') throw new RuntimeException("Internal Error"); + if (++this.depth > MAX_DEPTH) { + throw new ParseException(pos, ERROR_UNEXPECTED_JSON_DEPTH, c); + } + Object current = mapper.createArray(); read(); boolean needData = false; // @@ -249,6 +259,7 @@ abstract class JSONParserBase { case ']': if (needData && !acceptUselessComma) throw new ParseException(pos, ERROR_UNEXPECTED_CHAR, (char) c); + this.depth--; read(); /* unstack */ // return mapper.convert(current); @@ -485,6 +496,9 @@ abstract class JSONParserBase { // if (c != '{') throw new RuntimeException("Internal Error"); + if (++this.depth > MAX_DEPTH) { + throw new ParseException(pos, ERROR_UNEXPECTED_JSON_DEPTH, c); + } Object current = mapper.createObject(); boolean needData = false; boolean acceptData = true; @@ -504,6 +518,7 @@ abstract class JSONParserBase { case '}': if (needData && !acceptUselessComma) throw new ParseException(pos, ERROR_UNEXPECTED_CHAR, (char) c); + this.depth--; read(); /* unstack */ // return mapper.convert(current); diff --git a/json-smart/src/main/java/net/minidev/json/parser/ParseException.java b/json-smart/src/main/java/net/minidev/json/parser/ParseException.java index e652cf2..42f11f2 100644 --- a/json-smart/src/main/java/net/minidev/json/parser/ParseException.java +++ b/json-smart/src/main/java/net/minidev/json/parser/ParseException.java @@ -1,7 +1,7 @@ package net.minidev.json.parser; /* - * Copyright 2011 JSON-SMART authors + * Copyright 2011-2023 JSON-SMART authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ public class ParseException extends Exception { public static final int ERROR_UNEXPECTED_UNICODE = 4; public static final int ERROR_UNEXPECTED_DUPLICATE_KEY = 5; public static final int ERROR_UNEXPECTED_LEADING_0 = 6; + public static final int ERROR_UNEXPECTED_JSON_DEPTH = 7; private int errorType; private Object unexpectedObject; @@ -114,6 +115,12 @@ public class ParseException extends Exception { sb.append(" at position "); sb.append(position); sb.append("."); + } else if (errorType == ERROR_UNEXPECTED_JSON_DEPTH) { + sb.append("Malicious payload, having non natural depths, parsing stoped on "); + sb.append(unexpectedObject); + sb.append(" at position "); + sb.append(position); + sb.append("."); } else { sb.append("Unkown error at position "); sb.append(position); diff --git a/json-smart/src/test/java/net/minidev/json/test/TestOverflow.java b/json-smart/src/test/java/net/minidev/json/test/TestOverflow.java new file mode 100644 index 0000000..18b52e7 --- /dev/null +++ b/json-smart/src/test/java/net/minidev/json/test/TestOverflow.java @@ -0,0 +1,27 @@ +package net.minidev.json.test; + +import junit.framework.TestCase; +import net.minidev.json.JSONValue; +import net.minidev.json.parser.ParseException; + +public class TestOverflow extends TestCase { + public void testStress() throws Exception { + int size = 10000; + StringBuilder sb = new StringBuilder(10 + size*4); + for (int i=0; i < size; i++) { + sb.append("{a:"); + } + sb.append("true"); + for (int i=0; i < size; i++) { + sb.append("}"); + } + String s = sb.toString(); + try { + JSONValue.parseWithException(s); + } catch (ParseException e) { + assertEquals(e.getErrorType(), ParseException.ERROR_UNEXPECTED_JSON_DEPTH); + return; + } + assertEquals(0,1); + } +}