Skip to content

Commit

Permalink
#91: Stack overflow error caused by jakarta.json parsing of untruste…
Browse files Browse the repository at this point in the history
…d JSON String

Signed-off-by: Lukas Jungmann <[email protected]>
  • Loading branch information
lukasj committed Jul 12, 2023
1 parent 7151e55 commit d0ec79b
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 1 deletion.
12 changes: 12 additions & 0 deletions impl/src/main/java/org/eclipse/parsson/JsonContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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;

Expand All @@ -70,6 +76,7 @@ final class JsonContext {
JsonContext(Map<String, ?> 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);
Expand All @@ -86,6 +93,7 @@ final class JsonContext {
JsonContext(Map<String, ?> 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);
Expand All @@ -109,6 +117,10 @@ int bigDecimalLengthLimit() {
return bigDecimalLengthLimit;
}

int depthLimit() {
return depthLimit;
}

boolean prettyPrinting() {
return prettyPrinting;
}
Expand Down
15 changes: 14 additions & 1 deletion impl/src/main/java/org/eclipse/parsson/JsonParserImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,25 +55,28 @@ 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;

private final JsonContext jsonContext;

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

Expand Down Expand Up @@ -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;
}
Expand All @@ -391,6 +403,7 @@ private Context pop() {
if (head == null) {
throw new NoSuchElementException();
}
size--;
Context temp = head;
head = head.next;
return temp;
Expand Down
6 changes: 6 additions & 0 deletions impl/src/main/java/org/eclipse/parsson/api/JsonConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
93 changes: 93 additions & 0 deletions impl/src/test/java/org/eclipse/parsson/tests/JsonNestingTest.java
Original file line number Diff line number Diff line change
@@ -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();
}

}

0 comments on commit d0ec79b

Please sign in to comment.