Skip to content

Commit 1039427

Browse files
authored
Add nesting limit for JsonReader (#2588)
* Add nesting limit for `JsonReader` For now don't expose this as additional GsonBuilder method assuming that the default nesting limit is high enough for most users. Otherwise users can first obtain a JsonReader from `Gson.newJsonReader` and then set a custom nesting limit. * Ignore nesting limit for `JsonTreeReader` See comment in JsonTreeReaderTest for the rationale * Fix formatting * Add concrete example to nesting limit Javadoc * Add comment about location being slightly incorrect
1 parent 18cf79a commit 1039427

File tree

7 files changed

+228
-32
lines changed

7 files changed

+228
-32
lines changed

gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,15 @@ public void close() {
5151
};
5252
private static final Object SENTINEL_CLOSED = new Object();
5353

54-
/*
55-
* The nesting stack. Using a manual array rather than an ArrayList saves 20%.
56-
*/
54+
/** The nesting stack. Using a manual array rather than an ArrayList saves 20%. */
5755
private Object[] stack = new Object[32];
56+
57+
/**
58+
* The used size of {@link #stack}; the value at {@code stackSize - 1} is the value last placed on
59+
* the stack. {@code stackSize} might differ from the nesting depth, because the stack also
60+
* contains temporary additional objects, for example for a JsonArray it contains the JsonArray
61+
* object as well as the corresponding iterator.
62+
*/
5863
private int stackSize = 0;
5964

6065
/*

gson/src/main/java/com/google/gson/stream/JsonReader.java

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.google.gson.Gson;
2020
import com.google.gson.GsonBuilder;
2121
import com.google.gson.Strictness;
22+
import com.google.gson.TypeAdapter;
2223
import com.google.gson.internal.JsonReaderInternalAccess;
2324
import com.google.gson.internal.TroubleshootingGuide;
2425
import com.google.gson.internal.bind.JsonTreeReader;
@@ -70,6 +71,7 @@
7071
*
7172
* <ul>
7273
* <li>{@link #setStrictness(Strictness)}, the default is {@link Strictness#LEGACY_STRICT}
74+
* <li>{@link #setNestingLimit(int)}, the default is {@value #DEFAULT_NESTING_LIMIT}
7375
* </ul>
7476
*
7577
* The default configuration of {@code JsonReader} instances used internally by the {@link Gson}
@@ -250,6 +252,10 @@ public class JsonReader implements Closeable {
250252
private final Reader in;
251253

252254
private Strictness strictness = Strictness.LEGACY_STRICT;
255+
// Default nesting limit is based on
256+
// https://github.com/square/moshi/blob/parent-1.15.0/moshi/src/main/java/com/squareup/moshi/JsonReader.java#L228-L230
257+
private static final int DEFAULT_NESTING_LIMIT = 255;
258+
private int nestingLimit = DEFAULT_NESTING_LIMIT;
253259

254260
static final int BUFFER_SIZE = 1024;
255261

@@ -286,10 +292,9 @@ public class JsonReader implements Closeable {
286292
*/
287293
private String peekedString;
288294

289-
/*
290-
* The nesting stack. Using a manual array rather than an ArrayList saves 20%.
291-
*/
295+
/** The nesting stack. Using a manual array rather than an ArrayList saves 20%. */
292296
private int[] stack = new int[32];
297+
293298
private int stackSize = 0;
294299

295300
{
@@ -411,6 +416,41 @@ public final Strictness getStrictness() {
411416
return strictness;
412417
}
413418

419+
/**
420+
* Sets the nesting limit of this reader.
421+
*
422+
* <p>The nesting limit defines how many JSON arrays or objects may be open at the same time. For
423+
* example a nesting limit of 0 means no arrays or objects may be opened at all, a nesting limit
424+
* of 1 means one array or object may be open at the same time, and so on. So a nesting limit of 3
425+
* allows reading the JSON data <code>[{"a":[true]}]</code>, but for a nesting limit of 2 it would
426+
* fail at the inner {@code [true]}.
427+
*
428+
* <p>The nesting limit can help to protect against a {@link StackOverflowError} when recursive
429+
* {@link TypeAdapter} implementations process deeply nested JSON data.
430+
*
431+
* <p>The default nesting limit is {@value #DEFAULT_NESTING_LIMIT}.
432+
*
433+
* @throws IllegalArgumentException if the nesting limit is negative.
434+
* @since $next-version$
435+
* @see #getNestingLimit()
436+
*/
437+
public final void setNestingLimit(int limit) {
438+
if (limit < 0) {
439+
throw new IllegalArgumentException("Invalid nesting limit: " + limit);
440+
}
441+
this.nestingLimit = limit;
442+
}
443+
444+
/**
445+
* Returns the nesting limit of this reader.
446+
*
447+
* @since $next-version$
448+
* @see #setNestingLimit(int)
449+
*/
450+
public final int getNestingLimit() {
451+
return nestingLimit;
452+
}
453+
414454
/**
415455
* Consumes the next token from the JSON stream and asserts that it is the beginning of a new
416456
* array.
@@ -1408,7 +1448,13 @@ public void skipValue() throws IOException {
14081448
pathIndices[stackSize - 1]++;
14091449
}
14101450

1411-
private void push(int newTop) {
1451+
private void push(int newTop) throws MalformedJsonException {
1452+
// - 1 because stack contains as first element either EMPTY_DOCUMENT or NONEMPTY_DOCUMENT
1453+
if (stackSize - 1 >= nestingLimit) {
1454+
throw new MalformedJsonException(
1455+
"Nesting limit " + nestingLimit + " reached" + locationString());
1456+
}
1457+
14121458
if (stackSize == stack.length) {
14131459
int newLength = stackSize * 2;
14141460
stack = Arrays.copyOf(stack, newLength);

gson/src/test/java/com/google/gson/JsonParserTest.java

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -94,23 +94,17 @@ public void testParseMixedArray() {
9494
assertThat(array.get(2).getAsString()).isEqualTo("stringValue");
9595
}
9696

97-
private static String repeat(String s, int times) {
98-
StringBuilder stringBuilder = new StringBuilder(s.length() * times);
99-
for (int i = 0; i < times; i++) {
100-
stringBuilder.append(s);
101-
}
102-
return stringBuilder.toString();
103-
}
104-
10597
/** Deeply nested JSON arrays should not cause {@link StackOverflowError} */
10698
@Test
10799
public void testParseDeeplyNestedArrays() throws IOException {
108100
int times = 10000;
109101
// [[[ ... ]]]
110-
String json = repeat("[", times) + repeat("]", times);
102+
String json = "[".repeat(times) + "]".repeat(times);
103+
JsonReader jsonReader = new JsonReader(new StringReader(json));
104+
jsonReader.setNestingLimit(Integer.MAX_VALUE);
111105

112106
int actualTimes = 0;
113-
JsonArray current = JsonParser.parseString(json).getAsJsonArray();
107+
JsonArray current = JsonParser.parseReader(jsonReader).getAsJsonArray();
114108
while (true) {
115109
actualTimes++;
116110
if (current.isEmpty()) {
@@ -127,10 +121,12 @@ public void testParseDeeplyNestedArrays() throws IOException {
127121
public void testParseDeeplyNestedObjects() throws IOException {
128122
int times = 10000;
129123
// {"a":{"a": ... {"a":null} ... }}
130-
String json = repeat("{\"a\":", times) + "null" + repeat("}", times);
124+
String json = "{\"a\":".repeat(times) + "null" + "}".repeat(times);
125+
JsonReader jsonReader = new JsonReader(new StringReader(json));
126+
jsonReader.setNestingLimit(Integer.MAX_VALUE);
131127

132128
int actualTimes = 0;
133-
JsonObject current = JsonParser.parseString(json).getAsJsonObject();
129+
JsonObject current = JsonParser.parseReader(jsonReader).getAsJsonObject();
134130
while (true) {
135131
assertThat(current.size()).isEqualTo(1);
136132
actualTimes++;

gson/src/test/java/com/google/gson/ObjectTypeAdapterTest.java

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818

1919
import static com.google.common.truth.Truth.assertThat;
2020

21+
import com.google.gson.stream.JsonReader;
2122
import java.io.IOException;
23+
import java.io.StringReader;
2224
import java.util.Arrays;
2325
import java.util.Collections;
2426
import java.util.LinkedHashMap;
@@ -64,24 +66,18 @@ public void testSerializeObject() {
6466
assertThat(adapter.toJson(new Object())).isEqualTo("{}");
6567
}
6668

67-
private static String repeat(String s, int times) {
68-
StringBuilder stringBuilder = new StringBuilder(s.length() * times);
69-
for (int i = 0; i < times; i++) {
70-
stringBuilder.append(s);
71-
}
72-
return stringBuilder.toString();
73-
}
74-
7569
/** Deeply nested JSON arrays should not cause {@link StackOverflowError} */
7670
@SuppressWarnings("unchecked")
7771
@Test
7872
public void testDeserializeDeeplyNestedArrays() throws IOException {
7973
int times = 10000;
8074
// [[[ ... ]]]
81-
String json = repeat("[", times) + repeat("]", times);
75+
String json = "[".repeat(times) + "]".repeat(times);
76+
JsonReader jsonReader = new JsonReader(new StringReader(json));
77+
jsonReader.setNestingLimit(Integer.MAX_VALUE);
8278

8379
int actualTimes = 0;
84-
List<List<?>> current = (List<List<?>>) adapter.fromJson(json);
80+
List<List<?>> current = (List<List<?>>) adapter.read(jsonReader);
8581
while (true) {
8682
actualTimes++;
8783
if (current.isEmpty()) {
@@ -99,10 +95,12 @@ public void testDeserializeDeeplyNestedArrays() throws IOException {
9995
public void testDeserializeDeeplyNestedObjects() throws IOException {
10096
int times = 10000;
10197
// {"a":{"a": ... {"a":null} ... }}
102-
String json = repeat("{\"a\":", times) + "null" + repeat("}", times);
98+
String json = "{\"a\":".repeat(times) + "null" + "}".repeat(times);
99+
JsonReader jsonReader = new JsonReader(new StringReader(json));
100+
jsonReader.setNestingLimit(Integer.MAX_VALUE);
103101

104102
int actualTimes = 0;
105-
Map<String, Map<?, ?>> current = (Map<String, Map<?, ?>>) adapter.fromJson(json);
103+
Map<String, Map<?, ?>> current = (Map<String, Map<?, ?>>) adapter.read(jsonReader);
106104
while (current != null) {
107105
assertThat(current).hasSize(1);
108106
actualTimes++;

gson/src/test/java/com/google/gson/functional/ObjectTest.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import com.google.gson.JsonPrimitive;
3434
import com.google.gson.JsonSerializationContext;
3535
import com.google.gson.JsonSerializer;
36+
import com.google.gson.JsonSyntaxException;
3637
import com.google.gson.common.TestTypes.ArrayOfObjects;
3738
import com.google.gson.common.TestTypes.BagOfPrimitiveWrappers;
3839
import com.google.gson.common.TestTypes.BagOfPrimitives;
@@ -43,6 +44,7 @@
4344
import com.google.gson.common.TestTypes.Nested;
4445
import com.google.gson.common.TestTypes.PrimitiveArray;
4546
import com.google.gson.reflect.TypeToken;
47+
import com.google.gson.stream.MalformedJsonException;
4648
import java.io.EOFException;
4749
import java.lang.reflect.Type;
4850
import java.util.ArrayList;
@@ -767,4 +769,29 @@ public ClassWithThrowingConstructor() {
767769
throw thrownException;
768770
}
769771
}
772+
773+
@Test
774+
public void testDeeplyNested() {
775+
int defaultLimit = 255;
776+
// json = {"r":{"r": ... {"r":null} ... }}
777+
String json = "{\"r\":".repeat(defaultLimit) + "null" + "}".repeat(defaultLimit);
778+
RecursiveClass deserialized = gson.fromJson(json, RecursiveClass.class);
779+
assertThat(deserialized).isNotNull();
780+
assertThat(deserialized.r).isNotNull();
781+
782+
// json = {"r":{"r": ... {"r":null} ... }}
783+
String json2 = "{\"r\":".repeat(defaultLimit + 1) + "null" + "}".repeat(defaultLimit + 1);
784+
JsonSyntaxException e =
785+
assertThrows(JsonSyntaxException.class, () -> gson.fromJson(json2, RecursiveClass.class));
786+
assertThat(e).hasCauseThat().isInstanceOf(MalformedJsonException.class);
787+
assertThat(e)
788+
.hasCauseThat()
789+
.hasMessageThat()
790+
.isEqualTo(
791+
"Nesting limit 255 reached at line 1 column 1277 path $" + ".r".repeat(defaultLimit));
792+
}
793+
794+
private static class RecursiveClass {
795+
RecursiveClass r;
796+
}
770797
}

gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,51 @@ public JsonElement deepCopy() {
137137
"Custom JsonElement subclass " + CustomSubclass.class.getName() + " is not supported");
138138
}
139139

140+
/**
141+
* {@link JsonTreeReader} ignores nesting limit because:
142+
*
143+
* <ul>
144+
* <li>It is an internal class and often created implicitly without the user having access to it
145+
* (as {@link JsonReader}), so they cannot easily adjust the limit
146+
* <li>{@link JsonTreeReader} may be created based on an existing {@link JsonReader}; in that
147+
* case it would be necessary to propagate settings to account for a custom nesting limit,
148+
* see also related https://github.com/google/gson/pull/2151
149+
* <li>Nesting limit as protection against {@link StackOverflowError} is not that relevant for
150+
* {@link JsonTreeReader} because a deeply nested {@link JsonElement} tree would first have
151+
* to be constructed; and if it is constructed from a regular {@link JsonReader}, then its
152+
* nesting limit would already apply
153+
* </ul>
154+
*/
155+
@Test
156+
public void testNestingLimitIgnored() throws IOException {
157+
int limit = 10;
158+
JsonArray json = new JsonArray();
159+
JsonArray current = json;
160+
// This adds additional `limit` nested arrays, so in total there are `limit + 1` arrays
161+
for (int i = 0; i < limit; i++) {
162+
JsonArray nested = new JsonArray();
163+
current.add(nested);
164+
current = nested;
165+
}
166+
167+
JsonTreeReader reader = new JsonTreeReader(json);
168+
reader.setNestingLimit(limit);
169+
assertThat(reader.getNestingLimit()).isEqualTo(limit);
170+
171+
for (int i = 0; i < limit; i++) {
172+
reader.beginArray();
173+
}
174+
// Does not throw exception; limit is ignored
175+
reader.beginArray();
176+
177+
reader.endArray();
178+
for (int i = 0; i < limit; i++) {
179+
reader.endArray();
180+
}
181+
assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
182+
reader.close();
183+
}
184+
140185
/**
141186
* {@link JsonTreeReader} effectively replaces the complete reading logic of {@link JsonReader} to
142187
* read from a {@link JsonElement} instead of a {@link Reader}. Therefore all relevant methods of
@@ -149,7 +194,9 @@ public void testOverrides() {
149194
"setLenient(boolean)",
150195
"isLenient()",
151196
"setStrictness(com.google.gson.Strictness)",
152-
"getStrictness()");
197+
"getStrictness()",
198+
"setNestingLimit(int)",
199+
"getNestingLimit()");
153200
MoreAsserts.assertOverridesMethods(JsonReader.class, JsonTreeReader.class, ignoredMethods);
154201
}
155202
}

0 commit comments

Comments
 (0)