Skip to content

Commit dd31cf3

Browse files
committed
Guards against numbers causing CPU or OOM issues when deserializing large numbers into Instant or Duration by either: - Scientific notation too large (eg 10000e100000) - Raw string repesenting a number of length too long
1 parent cf53b82 commit dd31cf3

File tree

4 files changed

+180
-3
lines changed

4 files changed

+180
-3
lines changed

datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/DurationDeserializer.java

+24-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.fasterxml.jackson.datatype.jsr310.deser;
1818

19+
import com.fasterxml.jackson.core.JsonParseException;
1920
import com.fasterxml.jackson.core.JsonParser;
2021
import com.fasterxml.jackson.core.JsonToken;
2122
import com.fasterxml.jackson.core.JsonTokenId;
@@ -25,6 +26,8 @@
2526

2627
import java.io.IOException;
2728
import java.math.BigDecimal;
29+
import java.text.DecimalFormat;
30+
import java.text.NumberFormat;
2831
import java.time.DateTimeException;
2932
import java.time.Duration;
3033

@@ -40,6 +43,13 @@ public class DurationDeserializer extends JSR310DeserializerBase<Duration>
4043

4144
public static final DurationDeserializer INSTANCE = new DurationDeserializer();
4245

46+
private static final int INSTANT_MAX_STRING_LEN = java.time.Instant.MAX.toString().length();
47+
private static final BigDecimal INSTANT_MAX = new BigDecimal(
48+
java.time.Instant.MAX.getEpochSecond() + "." + java.time.Instant.MAX.getNano());
49+
private static final BigDecimal INSTANT_MIN = new BigDecimal(
50+
java.time.Instant.MIN.getEpochSecond() + "." + java.time.Instant.MIN.getNano());
51+
52+
4353
private DurationDeserializer()
4454
{
4555
super(Duration.class);
@@ -48,22 +58,35 @@ private DurationDeserializer()
4858
@Override
4959
public Duration deserialize(JsonParser parser, DeserializationContext context) throws IOException
5060
{
61+
String string = parser.getText().trim();
5162
switch (parser.getCurrentTokenId())
5263
{
5364
case JsonTokenId.ID_NUMBER_FLOAT:
5465
BigDecimal value = parser.getDecimalValue();
66+
// If the decimal isnt within the bounds of a float, bail out
67+
if(value.compareTo(INSTANT_MAX) > 0 ||
68+
value.compareTo(INSTANT_MIN) < 0) {
69+
NumberFormat formatter = new DecimalFormat("0.0E0");
70+
throw new JsonParseException(context.getParser(),
71+
String.format("Value of BigDecimal (%s) not within range to be converted to Duration",
72+
formatter.format(value)));
73+
}
74+
5575
long seconds = value.longValue();
5676
int nanoseconds = DecimalUtils.extractNanosecondDecimal(value, seconds);
5777
return Duration.ofSeconds(seconds, nanoseconds);
5878

5979
case JsonTokenId.ID_NUMBER_INT:
80+
if(string.length() > INSTANT_MAX_STRING_LEN) {
81+
throw new JsonParseException(context.getParser(),
82+
String.format("Value of Integer too large to be converted to Duration"));
83+
}
6084
if(context.isEnabled(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)) {
6185
return Duration.ofSeconds(parser.getLongValue());
6286
}
6387
return Duration.ofMillis(parser.getLongValue());
6488

6589
case JsonTokenId.ID_STRING:
66-
String string = parser.getText().trim();
6790
if (string.length() == 0) {
6891
return null;
6992
}

datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/InstantDeserializer.java

+27-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.fasterxml.jackson.datatype.jsr310.deser;
1818

1919
import com.fasterxml.jackson.annotation.JsonFormat;
20+
import com.fasterxml.jackson.core.JsonParseException;
2021
import com.fasterxml.jackson.core.JsonParser;
2122
import com.fasterxml.jackson.core.JsonToken;
2223
import com.fasterxml.jackson.core.JsonTokenId;
@@ -29,6 +30,8 @@
2930

3031
import java.io.IOException;
3132
import java.math.BigDecimal;
33+
import java.text.DecimalFormat;
34+
import java.text.NumberFormat;
3235
import java.time.DateTimeException;
3336
import java.time.Instant;
3437
import java.time.OffsetDateTime;
@@ -52,6 +55,12 @@ public class InstantDeserializer<T extends Temporal>
5255
{
5356
private static final long serialVersionUID = 1L;
5457

58+
private static final int INSTANT_MAX_STRING_LEN = java.time.Instant.MAX.toString().length();
59+
private static final BigDecimal INSTANT_MAX = new BigDecimal(
60+
java.time.Instant.MAX.getEpochSecond() + "." + java.time.Instant.MAX.getNano());
61+
private static final BigDecimal INSTANT_MIN = new BigDecimal(
62+
java.time.Instant.MIN.getEpochSecond() + "." + java.time.Instant.MIN.getNano());
63+
5564
/**
5665
* Constants used to check if the time offset is zero. See [jackson-modules-java8#18]
5766
*
@@ -165,17 +174,25 @@ public T deserialize(JsonParser parser, DeserializationContext context) throws I
165174
{
166175
//NOTE: Timestamps contain no timezone info, and are always in configured TZ. Only
167176
//string values have to be adjusted to the configured TZ.
177+
String string = parser.getText().trim();
168178
switch (parser.getCurrentTokenId())
169179
{
170180
case JsonTokenId.ID_NUMBER_FLOAT:
181+
if(string.length() > INSTANT_MAX_STRING_LEN) {
182+
throw new JsonParseException(context.getParser(),
183+
String.format("Value of Float too large to be converted to Instant"));
184+
}
171185
return _fromDecimal(context, parser.getDecimalValue());
172186

173187
case JsonTokenId.ID_NUMBER_INT:
188+
if(string.length() > INSTANT_MAX_STRING_LEN) {
189+
throw new JsonParseException(context.getParser(),
190+
String.format("Value of Integer too large to be converted to Instant"));
191+
}
174192
return _fromLong(context, parser.getLongValue());
175193

176194
case JsonTokenId.ID_STRING:
177195
{
178-
String string = parser.getText().trim();
179196
if (string.length() == 0) {
180197
return null;
181198
}
@@ -277,8 +294,16 @@ protected T _fromLong(DeserializationContext context, long timestamp)
277294
timestamp, this.getZone(context)));
278295
}
279296

280-
protected T _fromDecimal(DeserializationContext context, BigDecimal value)
297+
protected T _fromDecimal(DeserializationContext context, BigDecimal value) throws JsonParseException
281298
{
299+
// If the decimal isnt within the bounds of an Instant, bail out
300+
if(value.compareTo(INSTANT_MAX) > 0 ||
301+
value.compareTo(INSTANT_MIN) < 0) {
302+
NumberFormat formatter = new DecimalFormat("0.0E0");
303+
throw new JsonParseException(context.getParser(),
304+
String.format("Value of BigDecimal (%s) not within range to be converted to Instant",
305+
formatter.format(value)));
306+
}
282307
long seconds = value.longValue();
283308
int nanoseconds = DecimalUtils.extractNanosecondDecimal(value, seconds);
284309
return fromNanoseconds.apply(new FromDecimalArguments(

datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/TestDurationDeserialization.java

+50
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.fasterxml.jackson.datatype.jsr310;
22

33
import java.time.Duration;
4+
import java.time.Instant;
45
import java.time.temporal.TemporalAmount;
56

67
import org.junit.Test;
@@ -13,6 +14,7 @@
1314

1415
import com.fasterxml.jackson.databind.DeserializationFeature;
1516
import com.fasterxml.jackson.databind.JsonMappingException;
17+
import com.fasterxml.jackson.core.JsonParseException;
1618
import com.fasterxml.jackson.databind.ObjectMapper;
1719
import com.fasterxml.jackson.databind.ObjectReader;
1820
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
@@ -61,6 +63,19 @@ public void testDeserializationAsFloat04() throws Exception
6163
assertEquals("The value is not correct.", Duration.ofSeconds(13498L, 8374), value);
6264
}
6365

66+
/**
67+
* This test can potentially hang the VM, so exit if it doesn't finish
68+
* within a few seconds.
69+
* @throws Exception
70+
*/
71+
@Test(timeout=3000, expected = JsonParseException.class)
72+
public void testDeserializationAsFloatWhereStringTooLarge() throws Exception
73+
{
74+
String customDuration = "1000000000e1000000000";
75+
READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
76+
.readValue(customDuration);
77+
}
78+
6479
@Test
6580
public void testDeserializationAsInt01() throws Exception
6681
{
@@ -100,6 +115,41 @@ public void testDeserializationAsInt04() throws Exception
100115
assertEquals("The value is not correct.", Duration.ofSeconds(13498L, 0), value);
101116
}
102117

118+
/**
119+
* This test can potentially hang the VM, so exit if it doesn't finish
120+
* within a few seconds.
121+
*
122+
* @throws Exception
123+
*/
124+
@Test(timeout=3000, expected = JsonParseException.class)
125+
public void testDeserializationAsIntTooLarge01() throws Exception
126+
{
127+
Instant date = Instant.MAX;
128+
// Add in an few extra zeros to be longer than what an epoch should be
129+
String customInstant = date.getEpochSecond() +"0000000000000000."+ date.getNano();
130+
131+
READER.with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
132+
.readValue(customInstant);
133+
}
134+
135+
/**
136+
* This test can potentially hang the VM, so exit if it doesn't finish
137+
* within a few seconds.
138+
*
139+
* @throws Exception
140+
*/
141+
@Test(timeout=3000, expected = JsonParseException.class)
142+
public void testDeserializationAsIntTooLarge1() throws Exception {
143+
String number = "0000000000000000000000000";
144+
StringBuffer customInstant = new StringBuffer("1");
145+
for (int i = 0; i < 1_000; i++) {
146+
customInstant.append(number);
147+
148+
}
149+
READER.with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
150+
.readValue(customInstant.toString());
151+
}
152+
103153
@Test
104154
public void testDeserializationAsString01() throws Exception
105155
{

datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/TestInstantSerialization.java

+79
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.fasterxml.jackson.datatype.jsr310;
1818

1919
import com.fasterxml.jackson.annotation.JsonFormat;
20+
import com.fasterxml.jackson.core.JsonParseException;
2021
import com.fasterxml.jackson.databind.DeserializationFeature;
2122
import com.fasterxml.jackson.databind.ObjectMapper;
2223
import com.fasterxml.jackson.databind.SerializationFeature;
@@ -376,6 +377,84 @@ public void testDeserializationWithTypeInfo04() throws Exception
376377
assertEquals("The value is not correct.", date, value);
377378
}
378379

380+
/**
381+
* This should be within the range of a max Instant and should pass
382+
* @throws Exception
383+
*/
384+
@Test(timeout=3000)
385+
public void testDeserializationWithTypeInfo05() throws Exception
386+
{
387+
Instant date = Instant.MAX;
388+
String customInstant = date.getEpochSecond() +"."+ date.getNano();
389+
ObjectMapper m = newMapper()
390+
.addMixIn(Temporal.class, MockObjectConfiguration.class);
391+
Temporal value = m.readValue(
392+
"[\"" + Instant.class.getName() + "\","+customInstant+"]", Temporal.class
393+
);
394+
assertTrue("The value should be an Instant.", value instanceof Instant);
395+
assertEquals("The value is not correct.", date, value);
396+
}
397+
398+
/**
399+
* This test can potentially hang the VM, so exit if it doesn't finish
400+
* within a few seconds.
401+
*
402+
* @throws Exception
403+
*/
404+
@Test(timeout=3000, expected = JsonParseException.class)
405+
public void testDeserializationWithTypeInfoAndStringTooLarge01() throws Exception
406+
{
407+
String customInstant = "1000000000000e1000000000000";
408+
ObjectMapper m = newMapper()
409+
.addMixIn(Temporal.class, MockObjectConfiguration.class);
410+
m.readValue(
411+
"[\"" + Instant.class.getName() + "\","+customInstant+"]", Temporal.class
412+
);
413+
}
414+
415+
/**
416+
* This test can potentially hang the VM, so exit if it doesn't finish
417+
* within a few seconds.
418+
*
419+
* @throws Exception
420+
*/
421+
@Test(timeout=3000, expected = JsonParseException.class)
422+
public void testDeserializationWithTypeInfoAndStringTooLarge02() throws Exception
423+
{
424+
Instant date = Instant.MAX;
425+
// Add in an few extra zeros to be longer than what an epoch should be
426+
String customInstant = date.getEpochSecond() +"0000000000000000."+ date.getNano();
427+
ObjectMapper m = newMapper()
428+
.addMixIn(Temporal.class, MockObjectConfiguration.class);
429+
m.readValue(
430+
"[\"" + Instant.class.getName() + "\","+customInstant+"]", Temporal.class
431+
);
432+
}
433+
434+
/**
435+
* This test can potentially hang the VM, so exit if it doesn't finish
436+
* within a few seconds.
437+
*
438+
* @throws Exception
439+
*/
440+
@Test(timeout=3000, expected = JsonParseException.class)
441+
public void testDeserializationWithTypeInfoAndStringTooLarge03() throws Exception
442+
{
443+
// Build a big string by hand
444+
String number = "0000000000000000000000000";
445+
StringBuffer customInstant = new StringBuffer("1");
446+
for(int i = 0; i < 1_000; i++ ) {
447+
customInstant.append(number);
448+
}
449+
450+
ObjectMapper m = newMapper()
451+
.addMixIn(Temporal.class, MockObjectConfiguration.class);
452+
m.readValue(
453+
"[\"" + Instant.class.getName() + "\","+customInstant.toString()+"]", Temporal.class
454+
);
455+
}
456+
457+
379458
@Test
380459
public void testCustomPatternWithAnnotations01() throws Exception
381460
{

0 commit comments

Comments
 (0)