Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#91: Stack overflow error caused by jakarta.json parsing of untrusted JSON String #92

Merged
merged 1 commit into from
Jul 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
}

}
Loading