Skip to content

Commit 2d01d6a

Browse files
authored
Make Object and JsonElement deserialization iterative (#1912)
* Make Object and JsonElement deserialization iterative Often when Object and JsonElement are deserialized the format of the JSON data is unknown and it might come from an untrusted source. To avoid a StackOverflowError from maliciously crafted JSON, deserialize Object and JsonElement iteratively instead of recursively. Concept based on FasterXML/jackson-databind@51fd2fa But implementation is not based on it. * Improve imports grouping * Address review feedback
1 parent d2aee65 commit 2d01d6a

File tree

6 files changed

+370
-80
lines changed

6 files changed

+370
-80
lines changed

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

+87-30
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,19 @@
1717
package com.google.gson.internal.bind;
1818

1919
import com.google.gson.Gson;
20-
import com.google.gson.ToNumberStrategy;
2120
import com.google.gson.ToNumberPolicy;
21+
import com.google.gson.ToNumberStrategy;
2222
import com.google.gson.TypeAdapter;
2323
import com.google.gson.TypeAdapterFactory;
2424
import com.google.gson.internal.LinkedTreeMap;
2525
import com.google.gson.reflect.TypeToken;
2626
import com.google.gson.stream.JsonReader;
2727
import com.google.gson.stream.JsonToken;
2828
import com.google.gson.stream.JsonWriter;
29-
3029
import java.io.IOException;
30+
import java.util.ArrayDeque;
3131
import java.util.ArrayList;
32+
import java.util.Deque;
3233
import java.util.List;
3334
import java.util.Map;
3435

@@ -70,42 +71,98 @@ public static TypeAdapterFactory getFactory(ToNumberStrategy toNumberStrategy) {
7071
}
7172
}
7273

74+
/**
75+
* Tries to begin reading a JSON array or JSON object, returning {@code null} if
76+
* the next element is neither of those.
77+
*/
78+
private Object tryBeginNesting(JsonReader in, JsonToken peeked) throws IOException {
79+
switch (peeked) {
80+
case BEGIN_ARRAY:
81+
in.beginArray();
82+
return new ArrayList<>();
83+
case BEGIN_OBJECT:
84+
in.beginObject();
85+
return new LinkedTreeMap<>();
86+
default:
87+
return null;
88+
}
89+
}
90+
91+
/** Reads an {@code Object} which cannot have any nested elements */
92+
private Object readTerminal(JsonReader in, JsonToken peeked) throws IOException {
93+
switch (peeked) {
94+
case STRING:
95+
return in.nextString();
96+
case NUMBER:
97+
return toNumberStrategy.readNumber(in);
98+
case BOOLEAN:
99+
return in.nextBoolean();
100+
case NULL:
101+
in.nextNull();
102+
return null;
103+
default:
104+
// When read(JsonReader) is called with JsonReader in invalid state
105+
throw new IllegalStateException("Unexpected token: " + peeked);
106+
}
107+
}
108+
73109
@Override public Object read(JsonReader in) throws IOException {
74-
JsonToken token = in.peek();
75-
switch (token) {
76-
case BEGIN_ARRAY:
77-
List<Object> list = new ArrayList<>();
78-
in.beginArray();
79-
while (in.hasNext()) {
80-
list.add(read(in));
81-
}
82-
in.endArray();
83-
return list;
110+
// Either List or Map
111+
Object current;
112+
JsonToken peeked = in.peek();
113+
114+
current = tryBeginNesting(in, peeked);
115+
if (current == null) {
116+
return readTerminal(in, peeked);
117+
}
118+
119+
Deque<Object> stack = new ArrayDeque<>();
84120

85-
case BEGIN_OBJECT:
86-
Map<String, Object> map = new LinkedTreeMap<>();
87-
in.beginObject();
121+
while (true) {
88122
while (in.hasNext()) {
89-
map.put(in.nextName(), read(in));
90-
}
91-
in.endObject();
92-
return map;
123+
String name = null;
124+
// Name is only used for JSON object members
125+
if (current instanceof Map) {
126+
name = in.nextName();
127+
}
93128

94-
case STRING:
95-
return in.nextString();
129+
peeked = in.peek();
130+
Object value = tryBeginNesting(in, peeked);
131+
boolean isNesting = value != null;
96132

97-
case NUMBER:
98-
return toNumberStrategy.readNumber(in);
133+
if (value == null) {
134+
value = readTerminal(in, peeked);
135+
}
99136

100-
case BOOLEAN:
101-
return in.nextBoolean();
137+
if (current instanceof List) {
138+
@SuppressWarnings("unchecked")
139+
List<Object> list = (List<Object>) current;
140+
list.add(value);
141+
} else {
142+
@SuppressWarnings("unchecked")
143+
Map<String, Object> map = (Map<String, Object>) current;
144+
map.put(name, value);
145+
}
146+
147+
if (isNesting) {
148+
stack.addLast(current);
149+
current = value;
150+
}
151+
}
102152

103-
case NULL:
104-
in.nextNull();
105-
return null;
153+
// End current element
154+
if (current instanceof List) {
155+
in.endArray();
156+
} else {
157+
in.endObject();
158+
}
106159

107-
default:
108-
throw new IllegalStateException();
160+
if (stack.isEmpty()) {
161+
return current;
162+
} else {
163+
// Continue with enclosing element
164+
current = stack.removeLast();
165+
}
109166
}
110167
}
111168

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

+104-48
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,22 @@
1616

1717
package com.google.gson.internal.bind;
1818

19+
import com.google.gson.Gson;
20+
import com.google.gson.JsonArray;
21+
import com.google.gson.JsonElement;
22+
import com.google.gson.JsonIOException;
23+
import com.google.gson.JsonNull;
24+
import com.google.gson.JsonObject;
25+
import com.google.gson.JsonPrimitive;
26+
import com.google.gson.JsonSyntaxException;
27+
import com.google.gson.TypeAdapter;
28+
import com.google.gson.TypeAdapterFactory;
29+
import com.google.gson.annotations.SerializedName;
30+
import com.google.gson.internal.LazilyParsedNumber;
31+
import com.google.gson.reflect.TypeToken;
32+
import com.google.gson.stream.JsonReader;
33+
import com.google.gson.stream.JsonToken;
34+
import com.google.gson.stream.JsonWriter;
1935
import java.io.IOException;
2036
import java.lang.reflect.AccessibleObject;
2137
import java.lang.reflect.Field;
@@ -27,10 +43,12 @@
2743
import java.net.URL;
2844
import java.security.AccessController;
2945
import java.security.PrivilegedAction;
46+
import java.util.ArrayDeque;
3047
import java.util.ArrayList;
3148
import java.util.BitSet;
3249
import java.util.Calendar;
3350
import java.util.Currency;
51+
import java.util.Deque;
3452
import java.util.GregorianCalendar;
3553
import java.util.HashMap;
3654
import java.util.List;
@@ -42,23 +60,6 @@
4260
import java.util.concurrent.atomic.AtomicInteger;
4361
import java.util.concurrent.atomic.AtomicIntegerArray;
4462

45-
import com.google.gson.Gson;
46-
import com.google.gson.JsonArray;
47-
import com.google.gson.JsonElement;
48-
import com.google.gson.JsonIOException;
49-
import com.google.gson.JsonNull;
50-
import com.google.gson.JsonObject;
51-
import com.google.gson.JsonPrimitive;
52-
import com.google.gson.JsonSyntaxException;
53-
import com.google.gson.TypeAdapter;
54-
import com.google.gson.TypeAdapterFactory;
55-
import com.google.gson.annotations.SerializedName;
56-
import com.google.gson.internal.LazilyParsedNumber;
57-
import com.google.gson.reflect.TypeToken;
58-
import com.google.gson.stream.JsonReader;
59-
import com.google.gson.stream.JsonToken;
60-
import com.google.gson.stream.JsonWriter;
61-
6263
/**
6364
* Type adapters for basic types.
6465
*/
@@ -695,44 +696,99 @@ public void write(JsonWriter out, Locale value) throws IOException {
695696
public static final TypeAdapterFactory LOCALE_FACTORY = newFactory(Locale.class, LOCALE);
696697

697698
public static final TypeAdapter<JsonElement> JSON_ELEMENT = new TypeAdapter<JsonElement>() {
699+
/**
700+
* Tries to begin reading a JSON array or JSON object, returning {@code null} if
701+
* the next element is neither of those.
702+
*/
703+
private JsonElement tryBeginNesting(JsonReader in, JsonToken peeked) throws IOException {
704+
switch (peeked) {
705+
case BEGIN_ARRAY:
706+
in.beginArray();
707+
return new JsonArray();
708+
case BEGIN_OBJECT:
709+
in.beginObject();
710+
return new JsonObject();
711+
default:
712+
return null;
713+
}
714+
}
715+
716+
/** Reads a {@link JsonElement} which cannot have any nested elements */
717+
private JsonElement readTerminal(JsonReader in, JsonToken peeked) throws IOException {
718+
switch (peeked) {
719+
case STRING:
720+
return new JsonPrimitive(in.nextString());
721+
case NUMBER:
722+
String number = in.nextString();
723+
return new JsonPrimitive(new LazilyParsedNumber(number));
724+
case BOOLEAN:
725+
return new JsonPrimitive(in.nextBoolean());
726+
case NULL:
727+
in.nextNull();
728+
return JsonNull.INSTANCE;
729+
default:
730+
// When read(JsonReader) is called with JsonReader in invalid state
731+
throw new IllegalStateException("Unexpected token: " + peeked);
732+
}
733+
}
734+
698735
@Override public JsonElement read(JsonReader in) throws IOException {
699736
if (in instanceof JsonTreeReader) {
700737
return ((JsonTreeReader) in).nextJsonElement();
701738
}
702739

703-
switch (in.peek()) {
704-
case STRING:
705-
return new JsonPrimitive(in.nextString());
706-
case NUMBER:
707-
String number = in.nextString();
708-
return new JsonPrimitive(new LazilyParsedNumber(number));
709-
case BOOLEAN:
710-
return new JsonPrimitive(in.nextBoolean());
711-
case NULL:
712-
in.nextNull();
713-
return JsonNull.INSTANCE;
714-
case BEGIN_ARRAY:
715-
JsonArray array = new JsonArray();
716-
in.beginArray();
740+
// Either JsonArray or JsonObject
741+
JsonElement current;
742+
JsonToken peeked = in.peek();
743+
744+
current = tryBeginNesting(in, peeked);
745+
if (current == null) {
746+
return readTerminal(in, peeked);
747+
}
748+
749+
Deque<JsonElement> stack = new ArrayDeque<>();
750+
751+
while (true) {
717752
while (in.hasNext()) {
718-
array.add(read(in));
753+
String name = null;
754+
// Name is only used for JSON object members
755+
if (current instanceof JsonObject) {
756+
name = in.nextName();
757+
}
758+
759+
peeked = in.peek();
760+
JsonElement value = tryBeginNesting(in, peeked);
761+
boolean isNesting = value != null;
762+
763+
if (value == null) {
764+
value = readTerminal(in, peeked);
765+
}
766+
767+
if (current instanceof JsonArray) {
768+
((JsonArray) current).add(value);
769+
} else {
770+
((JsonObject) current).add(name, value);
771+
}
772+
773+
if (isNesting) {
774+
stack.addLast(current);
775+
current = value;
776+
}
719777
}
720-
in.endArray();
721-
return array;
722-
case BEGIN_OBJECT:
723-
JsonObject object = new JsonObject();
724-
in.beginObject();
725-
while (in.hasNext()) {
726-
object.add(in.nextName(), read(in));
778+
779+
// End current element
780+
if (current instanceof JsonArray) {
781+
in.endArray();
782+
} else {
783+
in.endObject();
784+
}
785+
786+
if (stack.isEmpty()) {
787+
return current;
788+
} else {
789+
// Continue with enclosing element
790+
current = stack.removeLast();
727791
}
728-
in.endObject();
729-
return object;
730-
case END_DOCUMENT:
731-
case NAME:
732-
case END_OBJECT:
733-
case END_ARRAY:
734-
default:
735-
throw new IllegalArgumentException();
736792
}
737793
}
738794

@@ -803,7 +859,7 @@ public EnumTypeAdapter(final Class<T> classOfT) {
803859
T constant = (T)(constantField.get(null));
804860
String name = constant.name();
805861
String toStringVal = constant.toString();
806-
862+
807863
SerializedName annotation = constantField.getAnnotation(SerializedName.class);
808864
if (annotation != null) {
809865
name = annotation.value();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.google.gson;
2+
3+
import static org.junit.Assert.assertEquals;
4+
5+
import java.io.IOException;
6+
import java.util.Arrays;
7+
import org.junit.Test;
8+
import org.junit.runner.RunWith;
9+
import org.junit.runners.Parameterized;
10+
import org.junit.runners.Parameterized.Parameter;
11+
import org.junit.runners.Parameterized.Parameters;
12+
13+
@RunWith(Parameterized.class)
14+
public class JsonParserParameterizedTest {
15+
@Parameters
16+
public static Iterable<String> data() {
17+
return Arrays.asList(
18+
"[]",
19+
"{}",
20+
"null",
21+
"1.0",
22+
"true",
23+
"\"string\"",
24+
"[true,1.0,null,{},2.0,{\"a\":[false]},[3.0,\"test\"],4.0]",
25+
"{\"\":1.0,\"a\":true,\"b\":null,\"c\":[],\"d\":{\"a1\":2.0,\"b2\":[true,{\"a3\":3.0}]},\"e\":[{\"f\":4.0},\"test\"]}"
26+
);
27+
}
28+
29+
private final TypeAdapter<JsonElement> adapter = new Gson().getAdapter(JsonElement.class);
30+
@Parameter
31+
public String json;
32+
33+
@Test
34+
public void testParse() throws IOException {
35+
JsonElement deserialized = JsonParser.parseString(json);
36+
String actualSerialized = adapter.toJson(deserialized);
37+
38+
// Serialized JsonElement should be the same as original JSON
39+
assertEquals(json, actualSerialized);
40+
}
41+
}

0 commit comments

Comments
 (0)