Skip to content

Commit 6a9d240

Browse files
authored
Add limits when deserializing BigDecimal and BigInteger (#2510)
* Add limits when deserializing `BigDecimal` and `BigInteger` * Use assertThrows * Don't check number limits in JsonReader
1 parent 802476a commit 6a9d240

File tree

7 files changed

+284
-182
lines changed

7 files changed

+284
-182
lines changed

gson/src/main/java/com/google/gson/JsonPrimitive.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.google.gson;
1818

1919
import com.google.gson.internal.LazilyParsedNumber;
20+
import com.google.gson.internal.NumberLimits;
2021
import java.math.BigDecimal;
2122
import java.math.BigInteger;
2223
import java.util.Objects;
@@ -172,7 +173,7 @@ public double getAsDouble() {
172173
*/
173174
@Override
174175
public BigDecimal getAsBigDecimal() {
175-
return value instanceof BigDecimal ? (BigDecimal) value : new BigDecimal(getAsString());
176+
return value instanceof BigDecimal ? (BigDecimal) value : NumberLimits.parseBigDecimal(getAsString());
176177
}
177178

178179
/**
@@ -184,7 +185,7 @@ public BigInteger getAsBigInteger() {
184185
? (BigInteger) value
185186
: isIntegral(this)
186187
? BigInteger.valueOf(this.getAsNumber().longValue())
187-
: new BigInteger(this.getAsString());
188+
: NumberLimits.parseBigInteger(this.getAsString());
188189
}
189190

190191
/**

gson/src/main/java/com/google/gson/ToNumberPolicy.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.google.gson;
1818

1919
import com.google.gson.internal.LazilyParsedNumber;
20+
import com.google.gson.internal.NumberLimits;
2021
import com.google.gson.stream.JsonReader;
2122
import com.google.gson.stream.MalformedJsonException;
2223
import java.io.IOException;
@@ -89,7 +90,7 @@ public enum ToNumberPolicy implements ToNumberStrategy {
8990
@Override public BigDecimal readNumber(JsonReader in) throws IOException {
9091
String value = in.nextString();
9192
try {
92-
return new BigDecimal(value);
93+
return NumberLimits.parseBigDecimal(value);
9394
} catch (NumberFormatException e) {
9495
throw new JsonParseException("Cannot parse " + value + "; at path " + in.getPreviousPath(), e);
9596
}

gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ public LazilyParsedNumber(String value) {
3535
this.value = value;
3636
}
3737

38+
private BigDecimal asBigDecimal() {
39+
return NumberLimits.parseBigDecimal(value);
40+
}
41+
3842
@Override
3943
public int intValue() {
4044
try {
@@ -43,7 +47,7 @@ public int intValue() {
4347
try {
4448
return (int) Long.parseLong(value);
4549
} catch (NumberFormatException nfe) {
46-
return new BigDecimal(value).intValue();
50+
return asBigDecimal().intValue();
4751
}
4852
}
4953
}
@@ -53,7 +57,7 @@ public long longValue() {
5357
try {
5458
return Long.parseLong(value);
5559
} catch (NumberFormatException e) {
56-
return new BigDecimal(value).longValue();
60+
return asBigDecimal().longValue();
5761
}
5862
}
5963

@@ -78,7 +82,7 @@ public String toString() {
7882
* deserialize it.
7983
*/
8084
private Object writeReplace() throws ObjectStreamException {
81-
return new BigDecimal(value);
85+
return asBigDecimal();
8286
}
8387

8488
private void readObject(ObjectInputStream in) throws IOException {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.google.gson.internal;
2+
3+
import java.math.BigDecimal;
4+
import java.math.BigInteger;
5+
6+
/**
7+
* This class enforces limits on numbers parsed from JSON to avoid potential performance
8+
* problems when extremely large numbers are used.
9+
*/
10+
public class NumberLimits {
11+
private NumberLimits() {
12+
}
13+
14+
private static final int MAX_NUMBER_STRING_LENGTH = 10_000;
15+
16+
private static void checkNumberStringLength(String s) {
17+
if (s.length() > MAX_NUMBER_STRING_LENGTH) {
18+
throw new NumberFormatException("Number string too large: " + s.substring(0, 30) + "...");
19+
}
20+
}
21+
22+
public static BigDecimal parseBigDecimal(String s) throws NumberFormatException {
23+
checkNumberStringLength(s);
24+
BigDecimal decimal = new BigDecimal(s);
25+
26+
// Cast to long to avoid issues with abs when value is Integer.MIN_VALUE
27+
if (Math.abs((long) decimal.scale()) >= 10_000) {
28+
throw new NumberFormatException("Number has unsupported scale: " + s);
29+
}
30+
return decimal;
31+
}
32+
33+
public static BigInteger parseBigInteger(String s) throws NumberFormatException {
34+
checkNumberStringLength(s);
35+
return new BigInteger(s);
36+
}
37+
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.google.gson.TypeAdapterFactory;
2929
import com.google.gson.annotations.SerializedName;
3030
import com.google.gson.internal.LazilyParsedNumber;
31+
import com.google.gson.internal.NumberLimits;
3132
import com.google.gson.internal.TroubleshootingGuide;
3233
import com.google.gson.reflect.TypeToken;
3334
import com.google.gson.stream.JsonReader;
@@ -437,7 +438,7 @@ public void write(JsonWriter out, String value) throws IOException {
437438
}
438439
String s = in.nextString();
439440
try {
440-
return new BigDecimal(s);
441+
return NumberLimits.parseBigDecimal(s);
441442
} catch (NumberFormatException e) {
442443
throw new JsonSyntaxException("Failed parsing '" + s + "' as BigDecimal; at path " + in.getPreviousPath(), e);
443444
}
@@ -456,7 +457,7 @@ public void write(JsonWriter out, String value) throws IOException {
456457
}
457458
String s = in.nextString();
458459
try {
459-
return new BigInteger(s);
460+
return NumberLimits.parseBigInteger(s);
460461
} catch (NumberFormatException e) {
461462
throw new JsonSyntaxException("Failed parsing '" + s + "' as BigInteger; at path " + in.getPreviousPath(), e);
462463
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package com.google.gson.functional;
2+
3+
import static com.google.common.truth.Truth.assertThat;
4+
import static org.junit.Assert.assertThrows;
5+
6+
import com.google.gson.Gson;
7+
import com.google.gson.JsonParseException;
8+
import com.google.gson.JsonPrimitive;
9+
import com.google.gson.JsonSyntaxException;
10+
import com.google.gson.ToNumberPolicy;
11+
import com.google.gson.ToNumberStrategy;
12+
import com.google.gson.TypeAdapter;
13+
import com.google.gson.internal.LazilyParsedNumber;
14+
import com.google.gson.stream.JsonReader;
15+
import com.google.gson.stream.JsonToken;
16+
import com.google.gson.stream.MalformedJsonException;
17+
import java.io.IOException;
18+
import java.io.ObjectOutputStream;
19+
import java.io.OutputStream;
20+
import java.io.StringReader;
21+
import java.math.BigDecimal;
22+
import java.math.BigInteger;
23+
import org.junit.Test;
24+
25+
public class NumberLimitsTest {
26+
private static final int MAX_LENGTH = 10_000;
27+
28+
private static JsonReader jsonReader(String json) {
29+
return new JsonReader(new StringReader(json));
30+
}
31+
32+
/**
33+
* Tests how {@link JsonReader} behaves for large numbers.
34+
*
35+
* <p>Currently {@link JsonReader} itself does not enforce any limits.
36+
* The reasons for this are:
37+
* <ul>
38+
* <li>Methods such as {@link JsonReader#nextDouble()} seem to have no problem
39+
* parsing extremely large or small numbers (it rounds to 0 or Infinity)
40+
* (to be verified?; if it had performance problems with certain numbers, then
41+
* it would affect other parts of Gson which parse as float or double as well)
42+
* <li>Enforcing limits only when a JSON number is encountered would be ineffective
43+
* unless users explicitly call {@link JsonReader#peek()} and check if the value
44+
* is a JSON number. Otherwise the limits could be circumvented because
45+
* {@link JsonReader#nextString()} reads both strings and numbers, and for
46+
* JSON strings no restrictions are enforced.
47+
* </ul>
48+
*/
49+
@Test
50+
public void testJsonReader() throws IOException {
51+
JsonReader reader = jsonReader("1".repeat(1000));
52+
assertThat(reader.peek()).isEqualTo(JsonToken.NUMBER);
53+
assertThat(reader.nextString()).isEqualTo("1".repeat(1000));
54+
55+
JsonReader reader2 = jsonReader("1".repeat(MAX_LENGTH + 1));
56+
// Currently JsonReader does not recognize large JSON numbers as numbers but treats them
57+
// as unquoted string
58+
MalformedJsonException e = assertThrows(MalformedJsonException.class, () -> reader2.peek());
59+
assertThat(e).hasMessageThat().startsWith("Use JsonReader.setStrictness(Strictness.LENIENT) to accept malformed JSON");
60+
61+
reader = jsonReader("1e9999");
62+
assertThat(reader.peek()).isEqualTo(JsonToken.NUMBER);
63+
assertThat(reader.nextString()).isEqualTo("1e9999");
64+
65+
reader = jsonReader("1e+9999");
66+
assertThat(reader.peek()).isEqualTo(JsonToken.NUMBER);
67+
assertThat(reader.nextString()).isEqualTo("1e+9999");
68+
69+
reader = jsonReader("1e10000");
70+
assertThat(reader.peek()).isEqualTo(JsonToken.NUMBER);
71+
assertThat(reader.nextString()).isEqualTo("1e10000");
72+
73+
reader = jsonReader("1e00001");
74+
assertThat(reader.peek()).isEqualTo(JsonToken.NUMBER);
75+
assertThat(reader.nextString()).isEqualTo("1e00001");
76+
}
77+
78+
@Test
79+
public void testJsonPrimitive() {
80+
assertThat(new JsonPrimitive("1".repeat(MAX_LENGTH)).getAsBigDecimal())
81+
.isEqualTo(new BigDecimal("1".repeat(MAX_LENGTH)));
82+
assertThat(new JsonPrimitive("1e9999").getAsBigDecimal())
83+
.isEqualTo(new BigDecimal("1e9999"));
84+
assertThat(new JsonPrimitive("1e-9999").getAsBigDecimal())
85+
.isEqualTo(new BigDecimal("1e-9999"));
86+
87+
NumberFormatException e = assertThrows(NumberFormatException.class,
88+
() -> new JsonPrimitive("1".repeat(MAX_LENGTH + 1)).getAsBigDecimal());
89+
assertThat(e).hasMessageThat().isEqualTo("Number string too large: 111111111111111111111111111111...");
90+
91+
e = assertThrows(NumberFormatException.class,
92+
() -> new JsonPrimitive("1e10000").getAsBigDecimal());
93+
assertThat(e).hasMessageThat().isEqualTo("Number has unsupported scale: 1e10000");
94+
95+
e = assertThrows(NumberFormatException.class,
96+
() -> new JsonPrimitive("1e-10000").getAsBigDecimal());
97+
assertThat(e).hasMessageThat().isEqualTo("Number has unsupported scale: 1e-10000");
98+
99+
100+
assertThat(new JsonPrimitive("1".repeat(MAX_LENGTH)).getAsBigInteger())
101+
.isEqualTo(new BigInteger("1".repeat(MAX_LENGTH)));
102+
103+
e = assertThrows(NumberFormatException.class,
104+
() -> new JsonPrimitive("1".repeat(MAX_LENGTH + 1)).getAsBigInteger());
105+
assertThat(e).hasMessageThat().isEqualTo("Number string too large: 111111111111111111111111111111...");
106+
}
107+
108+
@Test
109+
public void testToNumberPolicy() throws IOException {
110+
ToNumberStrategy strategy = ToNumberPolicy.BIG_DECIMAL;
111+
112+
assertThat(strategy.readNumber(jsonReader("\"" + "1".repeat(MAX_LENGTH) + "\"")))
113+
.isEqualTo(new BigDecimal("1".repeat(MAX_LENGTH)));
114+
assertThat(strategy.readNumber(jsonReader("1e9999")))
115+
.isEqualTo(new BigDecimal("1e9999"));
116+
117+
118+
JsonParseException e = assertThrows(JsonParseException.class,
119+
() -> strategy.readNumber(jsonReader("\"" + "1".repeat(MAX_LENGTH + 1) + "\"")));
120+
assertThat(e).hasMessageThat().isEqualTo("Cannot parse " + "1".repeat(MAX_LENGTH + 1) + "; at path $");
121+
assertThat(e).hasCauseThat().hasMessageThat().isEqualTo("Number string too large: 111111111111111111111111111111...");
122+
123+
e = assertThrows(JsonParseException.class, () -> strategy.readNumber(jsonReader("\"1e10000\"")));
124+
assertThat(e).hasMessageThat().isEqualTo("Cannot parse 1e10000; at path $");
125+
assertThat(e).hasCauseThat().hasMessageThat().isEqualTo("Number has unsupported scale: 1e10000");
126+
}
127+
128+
@Test
129+
public void testLazilyParsedNumber() throws IOException {
130+
assertThat(new LazilyParsedNumber("1".repeat(MAX_LENGTH)).intValue())
131+
.isEqualTo(new BigDecimal("1".repeat(MAX_LENGTH)).intValue());
132+
assertThat(new LazilyParsedNumber("1e9999").intValue())
133+
.isEqualTo(new BigDecimal("1e9999").intValue());
134+
135+
NumberFormatException e = assertThrows(NumberFormatException.class,
136+
() -> new LazilyParsedNumber("1".repeat(MAX_LENGTH + 1)).intValue());
137+
assertThat(e).hasMessageThat().isEqualTo("Number string too large: 111111111111111111111111111111...");
138+
139+
e = assertThrows(NumberFormatException.class,
140+
() -> new LazilyParsedNumber("1e10000").intValue());
141+
assertThat(e).hasMessageThat().isEqualTo("Number has unsupported scale: 1e10000");
142+
143+
e = assertThrows(NumberFormatException.class,
144+
() -> new LazilyParsedNumber("1e10000").longValue());
145+
assertThat(e).hasMessageThat().isEqualTo("Number has unsupported scale: 1e10000");
146+
147+
ObjectOutputStream objOut = new ObjectOutputStream(OutputStream.nullOutputStream());
148+
// Number is serialized as BigDecimal; should also enforce limits during this conversion
149+
e = assertThrows(NumberFormatException.class, () -> objOut.writeObject(new LazilyParsedNumber("1e10000")));
150+
assertThat(e).hasMessageThat().isEqualTo("Number has unsupported scale: 1e10000");
151+
}
152+
153+
@Test
154+
public void testBigDecimalAdapter() throws IOException {
155+
TypeAdapter<BigDecimal> adapter = new Gson().getAdapter(BigDecimal.class);
156+
157+
assertThat(adapter.fromJson("\"" + "1".repeat(MAX_LENGTH) + "\""))
158+
.isEqualTo(new BigDecimal("1".repeat(MAX_LENGTH)));
159+
assertThat(adapter.fromJson("\"1e9999\""))
160+
.isEqualTo(new BigDecimal("1e9999"));
161+
162+
JsonSyntaxException e = assertThrows(JsonSyntaxException.class,
163+
() -> adapter.fromJson("\"" + "1".repeat(MAX_LENGTH + 1) + "\""));
164+
assertThat(e).hasMessageThat().isEqualTo("Failed parsing '" + "1".repeat(MAX_LENGTH + 1) + "' as BigDecimal; at path $");
165+
assertThat(e).hasCauseThat().hasMessageThat().isEqualTo("Number string too large: 111111111111111111111111111111...");
166+
167+
e = assertThrows(JsonSyntaxException.class,
168+
() -> adapter.fromJson("\"1e10000\""));
169+
assertThat(e).hasMessageThat().isEqualTo("Failed parsing '1e10000' as BigDecimal; at path $");
170+
assertThat(e).hasCauseThat().hasMessageThat().isEqualTo("Number has unsupported scale: 1e10000");
171+
}
172+
173+
@Test
174+
public void testBigIntegerAdapter() throws IOException {
175+
TypeAdapter<BigInteger> adapter = new Gson().getAdapter(BigInteger.class);
176+
177+
assertThat(adapter.fromJson("\"" + "1".repeat(MAX_LENGTH) + "\""))
178+
.isEqualTo(new BigInteger("1".repeat(MAX_LENGTH)));
179+
180+
JsonSyntaxException e = assertThrows(JsonSyntaxException.class,
181+
() -> adapter.fromJson("\"" + "1".repeat(MAX_LENGTH + 1) + "\""));
182+
assertThat(e).hasMessageThat().isEqualTo("Failed parsing '" + "1".repeat(MAX_LENGTH + 1) + "' as BigInteger; at path $");
183+
assertThat(e).hasCauseThat().hasMessageThat().isEqualTo("Number string too large: 111111111111111111111111111111...");
184+
}
185+
}

0 commit comments

Comments
 (0)