Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
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
  • Loading branch information
abracadv8 committed Oct 2, 2018
1 parent cf53b82 commit dd31cf3
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

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

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.JsonTokenId;
Expand All @@ -25,6 +26,8 @@

import java.io.IOException;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.time.DateTimeException;
import java.time.Duration;

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

public static final DurationDeserializer INSTANCE = new DurationDeserializer();

private static final int INSTANT_MAX_STRING_LEN = java.time.Instant.MAX.toString().length();
private static final BigDecimal INSTANT_MAX = new BigDecimal(
java.time.Instant.MAX.getEpochSecond() + "." + java.time.Instant.MAX.getNano());
private static final BigDecimal INSTANT_MIN = new BigDecimal(
java.time.Instant.MIN.getEpochSecond() + "." + java.time.Instant.MIN.getNano());


private DurationDeserializer()
{
super(Duration.class);
Expand All @@ -48,22 +58,35 @@ private DurationDeserializer()
@Override
public Duration deserialize(JsonParser parser, DeserializationContext context) throws IOException
{
String string = parser.getText().trim();
switch (parser.getCurrentTokenId())
{
case JsonTokenId.ID_NUMBER_FLOAT:
BigDecimal value = parser.getDecimalValue();
// If the decimal isnt within the bounds of a float, bail out
if(value.compareTo(INSTANT_MAX) > 0 ||
value.compareTo(INSTANT_MIN) < 0) {
NumberFormat formatter = new DecimalFormat("0.0E0");
throw new JsonParseException(context.getParser(),
String.format("Value of BigDecimal (%s) not within range to be converted to Duration",
formatter.format(value)));
}

long seconds = value.longValue();
int nanoseconds = DecimalUtils.extractNanosecondDecimal(value, seconds);
return Duration.ofSeconds(seconds, nanoseconds);

case JsonTokenId.ID_NUMBER_INT:
if(string.length() > INSTANT_MAX_STRING_LEN) {
throw new JsonParseException(context.getParser(),
String.format("Value of Integer too large to be converted to Duration"));
}
if(context.isEnabled(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)) {
return Duration.ofSeconds(parser.getLongValue());
}
return Duration.ofMillis(parser.getLongValue());

case JsonTokenId.ID_STRING:
String string = parser.getText().trim();
if (string.length() == 0) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.fasterxml.jackson.datatype.jsr310.deser;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.JsonTokenId;
Expand All @@ -29,6 +30,8 @@

import java.io.IOException;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.time.DateTimeException;
import java.time.Instant;
import java.time.OffsetDateTime;
Expand All @@ -52,6 +55,12 @@ public class InstantDeserializer<T extends Temporal>
{
private static final long serialVersionUID = 1L;

private static final int INSTANT_MAX_STRING_LEN = java.time.Instant.MAX.toString().length();
private static final BigDecimal INSTANT_MAX = new BigDecimal(
java.time.Instant.MAX.getEpochSecond() + "." + java.time.Instant.MAX.getNano());
private static final BigDecimal INSTANT_MIN = new BigDecimal(
java.time.Instant.MIN.getEpochSecond() + "." + java.time.Instant.MIN.getNano());

/**
* Constants used to check if the time offset is zero. See [jackson-modules-java8#18]
*
Expand Down Expand Up @@ -165,17 +174,25 @@ public T deserialize(JsonParser parser, DeserializationContext context) throws I
{
//NOTE: Timestamps contain no timezone info, and are always in configured TZ. Only
//string values have to be adjusted to the configured TZ.
String string = parser.getText().trim();
switch (parser.getCurrentTokenId())
{
case JsonTokenId.ID_NUMBER_FLOAT:
if(string.length() > INSTANT_MAX_STRING_LEN) {
throw new JsonParseException(context.getParser(),
String.format("Value of Float too large to be converted to Instant"));
}
return _fromDecimal(context, parser.getDecimalValue());

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

case JsonTokenId.ID_STRING:
{
String string = parser.getText().trim();
if (string.length() == 0) {
return null;
}
Expand Down Expand Up @@ -277,8 +294,16 @@ protected T _fromLong(DeserializationContext context, long timestamp)
timestamp, this.getZone(context)));
}

protected T _fromDecimal(DeserializationContext context, BigDecimal value)
protected T _fromDecimal(DeserializationContext context, BigDecimal value) throws JsonParseException
{
// If the decimal isnt within the bounds of an Instant, bail out
if(value.compareTo(INSTANT_MAX) > 0 ||
value.compareTo(INSTANT_MIN) < 0) {
NumberFormat formatter = new DecimalFormat("0.0E0");
throw new JsonParseException(context.getParser(),
String.format("Value of BigDecimal (%s) not within range to be converted to Instant",
formatter.format(value)));
}
long seconds = value.longValue();
int nanoseconds = DecimalUtils.extractNanosecondDecimal(value, seconds);
return fromNanoseconds.apply(new FromDecimalArguments(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.fasterxml.jackson.datatype.jsr310;

import java.time.Duration;
import java.time.Instant;
import java.time.temporal.TemporalAmount;

import org.junit.Test;
Expand All @@ -13,6 +14,7 @@

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
Expand Down Expand Up @@ -61,6 +63,19 @@ public void testDeserializationAsFloat04() throws Exception
assertEquals("The value is not correct.", Duration.ofSeconds(13498L, 8374), value);
}

/**
* This test can potentially hang the VM, so exit if it doesn't finish
* within a few seconds.
* @throws Exception
*/
@Test(timeout=3000, expected = JsonParseException.class)
public void testDeserializationAsFloatWhereStringTooLarge() throws Exception
{
String customDuration = "1000000000e1000000000";
READER.without(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
.readValue(customDuration);
}

@Test
public void testDeserializationAsInt01() throws Exception
{
Expand Down Expand Up @@ -100,6 +115,41 @@ public void testDeserializationAsInt04() throws Exception
assertEquals("The value is not correct.", Duration.ofSeconds(13498L, 0), value);
}

/**
* This test can potentially hang the VM, so exit if it doesn't finish
* within a few seconds.
*
* @throws Exception
*/
@Test(timeout=3000, expected = JsonParseException.class)
public void testDeserializationAsIntTooLarge01() throws Exception
{
Instant date = Instant.MAX;
// Add in an few extra zeros to be longer than what an epoch should be
String customInstant = date.getEpochSecond() +"0000000000000000."+ date.getNano();

READER.with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
.readValue(customInstant);
}

/**
* This test can potentially hang the VM, so exit if it doesn't finish
* within a few seconds.
*
* @throws Exception
*/
@Test(timeout=3000, expected = JsonParseException.class)
public void testDeserializationAsIntTooLarge1() throws Exception {
String number = "0000000000000000000000000";
StringBuffer customInstant = new StringBuffer("1");
for (int i = 0; i < 1_000; i++) {
customInstant.append(number);

}
READER.with(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
.readValue(customInstant.toString());
}

@Test
public void testDeserializationAsString01() throws Exception
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.fasterxml.jackson.datatype.jsr310;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
Expand Down Expand Up @@ -376,6 +377,84 @@ public void testDeserializationWithTypeInfo04() throws Exception
assertEquals("The value is not correct.", date, value);
}

/**
* This should be within the range of a max Instant and should pass
* @throws Exception
*/
@Test(timeout=3000)
public void testDeserializationWithTypeInfo05() throws Exception
{
Instant date = Instant.MAX;
String customInstant = date.getEpochSecond() +"."+ date.getNano();
ObjectMapper m = newMapper()
.addMixIn(Temporal.class, MockObjectConfiguration.class);
Temporal value = m.readValue(
"[\"" + Instant.class.getName() + "\","+customInstant+"]", Temporal.class
);
assertTrue("The value should be an Instant.", value instanceof Instant);
assertEquals("The value is not correct.", date, value);
}

/**
* This test can potentially hang the VM, so exit if it doesn't finish
* within a few seconds.
*
* @throws Exception
*/
@Test(timeout=3000, expected = JsonParseException.class)
public void testDeserializationWithTypeInfoAndStringTooLarge01() throws Exception
{
String customInstant = "1000000000000e1000000000000";
ObjectMapper m = newMapper()
.addMixIn(Temporal.class, MockObjectConfiguration.class);
m.readValue(
"[\"" + Instant.class.getName() + "\","+customInstant+"]", Temporal.class
);
}

/**
* This test can potentially hang the VM, so exit if it doesn't finish
* within a few seconds.
*
* @throws Exception
*/
@Test(timeout=3000, expected = JsonParseException.class)
public void testDeserializationWithTypeInfoAndStringTooLarge02() throws Exception
{
Instant date = Instant.MAX;
// Add in an few extra zeros to be longer than what an epoch should be
String customInstant = date.getEpochSecond() +"0000000000000000."+ date.getNano();
ObjectMapper m = newMapper()
.addMixIn(Temporal.class, MockObjectConfiguration.class);
m.readValue(
"[\"" + Instant.class.getName() + "\","+customInstant+"]", Temporal.class
);
}

/**
* This test can potentially hang the VM, so exit if it doesn't finish
* within a few seconds.
*
* @throws Exception
*/
@Test(timeout=3000, expected = JsonParseException.class)
public void testDeserializationWithTypeInfoAndStringTooLarge03() throws Exception
{
// Build a big string by hand
String number = "0000000000000000000000000";
StringBuffer customInstant = new StringBuffer("1");
for(int i = 0; i < 1_000; i++ ) {
customInstant.append(number);
}

ObjectMapper m = newMapper()
.addMixIn(Temporal.class, MockObjectConfiguration.class);
m.readValue(
"[\"" + Instant.class.getName() + "\","+customInstant.toString()+"]", Temporal.class
);
}


@Test
public void testCustomPatternWithAnnotations01() throws Exception
{
Expand Down

0 comments on commit dd31cf3

Please sign in to comment.