From d66653d616bb8054fdaa39cb746f169448c0bb7b Mon Sep 17 00:00:00 2001 From: Lukas Jungmann Date: Wed, 12 Jul 2023 16:41:15 +0200 Subject: [PATCH] #91: Stack overflow error caused by jakarta.json parsing of untrusted JSON String Signed-off-by: Lukas Jungmann --- .../java/org/eclipse/parsson/JsonContext.java | 12 +++ .../org/eclipse/parsson/JsonParserImpl.java | 15 ++- .../org/eclipse/parsson/api/JsonConfig.java | 6 ++ .../parsson/tests/JsonNestingTest.java | 93 +++++++++++++++++++ 4 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 impl/src/test/java/org/eclipse/parsson/tests/JsonNestingTest.java diff --git a/impl/src/main/java/org/eclipse/parsson/JsonContext.java b/impl/src/main/java/org/eclipse/parsson/JsonContext.java index 5ec77fbe..dc6e1594 100644 --- a/impl/src/main/java/org/eclipse/parsson/JsonContext.java +++ b/impl/src/main/java/org/eclipse/parsson/JsonContext.java @@ -40,6 +40,9 @@ final class JsonContext { /** Default maximum number of characters of BigDecimal source being parsed. */ private static final int DEFAULT_MAX_BIGDECIMAL_LEN = 1100; + /** Default maximum level of nesting. */ + private static final int DEFAULT_MAX_DEPTH = 1000; + /** * Custom char[] pool instance property. Can be set in properties {@code Map} only. */ @@ -53,6 +56,9 @@ final class JsonContext { // Maximum number of characters of BigDecimal source private final int bigDecimalLengthLimit; + // Maximum depth to parse + private final int depthLimit; + // Whether JSON pretty printing is enabled private final boolean prettyPrinting; @@ -70,6 +76,7 @@ final class JsonContext { JsonContext(Map config, BufferPool defaultPool) { this.bigIntegerScaleLimit = getIntConfig(JsonConfig.MAX_BIGINTEGER_SCALE, config, DEFAULT_MAX_BIGINTEGER_SCALE); this.bigDecimalLengthLimit = getIntConfig(JsonConfig.MAX_BIGDECIMAL_LEN, config, DEFAULT_MAX_BIGDECIMAL_LEN); + this.depthLimit = getIntConfig(JsonConfig.MAX_DEPTH, config, DEFAULT_MAX_DEPTH); this.prettyPrinting = getBooleanConfig(JsonGenerator.PRETTY_PRINTING, config); this.rejectDuplicateKeys = getBooleanConfig(JsonConfig.REJECT_DUPLICATE_KEYS, config); this.bufferPool = getBufferPool(config, defaultPool); @@ -86,6 +93,7 @@ final class JsonContext { JsonContext(Map config, BufferPool defaultPool, String... properties) { this.bigIntegerScaleLimit = getIntConfig(JsonConfig.MAX_BIGINTEGER_SCALE, config, DEFAULT_MAX_BIGINTEGER_SCALE); this.bigDecimalLengthLimit = getIntConfig(JsonConfig.MAX_BIGDECIMAL_LEN, config, DEFAULT_MAX_BIGDECIMAL_LEN); + this.depthLimit = getIntConfig(JsonConfig.MAX_DEPTH, config, DEFAULT_MAX_DEPTH); this.prettyPrinting = getBooleanConfig(JsonGenerator.PRETTY_PRINTING, config); this.rejectDuplicateKeys = getBooleanConfig(JsonConfig.REJECT_DUPLICATE_KEYS, config); this.bufferPool = getBufferPool(config, defaultPool); @@ -109,6 +117,10 @@ int bigDecimalLengthLimit() { return bigDecimalLengthLimit; } + int depthLimit() { + return depthLimit; + } + boolean prettyPrinting() { return prettyPrinting; } diff --git a/impl/src/main/java/org/eclipse/parsson/JsonParserImpl.java b/impl/src/main/java/org/eclipse/parsson/JsonParserImpl.java index 64817f7f..5c0002c9 100644 --- a/impl/src/main/java/org/eclipse/parsson/JsonParserImpl.java +++ b/impl/src/main/java/org/eclipse/parsson/JsonParserImpl.java @@ -55,7 +55,7 @@ public class JsonParserImpl implements JsonParser { private Context currentContext = new NoneContext(); private Event currentEvent; - private final Stack stack = new Stack(); + private final Stack stack; private final JsonTokenizer tokenizer; private boolean closed = false; @@ -63,17 +63,20 @@ public class JsonParserImpl implements JsonParser { public JsonParserImpl(Reader reader, JsonContext jsonContext) { this.jsonContext = jsonContext; + stack = new Stack(jsonContext.depthLimit()); this.tokenizer = new JsonTokenizer(reader, jsonContext); } public JsonParserImpl(InputStream in, JsonContext jsonContext) { this.jsonContext = jsonContext; + stack = new Stack(jsonContext.depthLimit()); UnicodeDetectingInputStream uin = new UnicodeDetectingInputStream(in); this.tokenizer = new JsonTokenizer(new InputStreamReader(uin, uin.getCharset()), jsonContext); } public JsonParserImpl(InputStream in, Charset encoding, JsonContext jsonContext) { this.jsonContext = jsonContext; + stack = new Stack(jsonContext.depthLimit()); this.tokenizer = new JsonTokenizer(new InputStreamReader(in, encoding), jsonContext); } @@ -380,9 +383,18 @@ public void close() { // Using the optimized stack impl as we don't require other things // like iterator etc. private static final class Stack { + int size = 0; + final int limit; private Context head; + Stack(int size) { + this.limit = size; + } + private void push(Context context) { + if (++size >= limit) { + throw new RuntimeException("Input is too deeply nested " + size); + } context.next = head; head = context; } @@ -391,6 +403,7 @@ private Context pop() { if (head == null) { throw new NoSuchElementException(); } + size--; Context temp = head; head = head.next; return temp; diff --git a/impl/src/main/java/org/eclipse/parsson/api/JsonConfig.java b/impl/src/main/java/org/eclipse/parsson/api/JsonConfig.java index 9b71959a..343a255c 100644 --- a/impl/src/main/java/org/eclipse/parsson/api/JsonConfig.java +++ b/impl/src/main/java/org/eclipse/parsson/api/JsonConfig.java @@ -34,6 +34,12 @@ public interface JsonConfig { */ String MAX_BIGDECIMAL_LEN = "org.eclipse.parsson.maxBigDecimalLength"; + /** + * Configuration property to limit maximum level of nesting when being parsing JSON string. + * Default value is set to {@code 1000}. + */ + String MAX_DEPTH = "org.eclipse.parsson.maxDepth"; + /** * Configuration property to reject duplicate keys. * The value of the property could be anything. diff --git a/impl/src/test/java/org/eclipse/parsson/tests/JsonNestingTest.java b/impl/src/test/java/org/eclipse/parsson/tests/JsonNestingTest.java new file mode 100644 index 00000000..146c87d3 --- /dev/null +++ b/impl/src/test/java/org/eclipse/parsson/tests/JsonNestingTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.eclipse.parsson.tests; + +import jakarta.json.Json; +import jakarta.json.stream.JsonParser; +import org.junit.Test; + +import java.io.StringReader; + +public class JsonNestingTest { + + @Test(expected = RuntimeException.class) + public void testArrayNestingException() { + String json = createDeepNestedDoc(500); + try (JsonParser parser = Json.createParser(new StringReader(json))) { + while (parser.hasNext()) { + JsonParser.Event ev = parser.next(); + if (JsonParser.Event.START_ARRAY == ev) { + parser.getArray(); + } + } + } + } + + @Test + public void testArrayNesting() { + String json = createDeepNestedDoc(499); + try (JsonParser parser = Json.createParser(new StringReader(json))) { + while (parser.hasNext()) { + JsonParser.Event ev = parser.next(); + if (JsonParser.Event.START_ARRAY == ev) { + parser.getArray(); + } + } + } + } + + @Test(expected = RuntimeException.class) + public void testObjectNestingException() { + String json = createDeepNestedDoc(500); + try (JsonParser parser = Json.createParser(new StringReader(json))) { + while (parser.hasNext()) { + JsonParser.Event ev = parser.next(); + if (JsonParser.Event.START_OBJECT == ev) { + parser.getObject(); + } + } + } + } + + @Test + public void testObjectNesting() { + String json = createDeepNestedDoc(499); + try (JsonParser parser = Json.createParser(new StringReader(json))) { + while (parser.hasNext()) { + JsonParser.Event ev = parser.next(); + if (JsonParser.Event.START_OBJECT == ev) { + parser.getObject(); + } + } + } + } + + private static String createDeepNestedDoc(final int depth) { + StringBuilder sb = new StringBuilder(); + sb.append("["); + for (int i = 0; i < depth; i++) { + sb.append("{ \"a\": ["); + } + sb.append(" \"val\" "); + for (int i = 0; i < depth; i++) { + sb.append("]}"); + } + sb.append("]"); + return sb.toString(); + } + +}