From dd31cf332c45122ef3537e9aedd5118c39f9afbd Mon Sep 17 00:00:00 2001 From: abracadvr8 Date: Tue, 2 Oct 2018 01:33:18 +0100 Subject: [PATCH] code to handle FasterXML/jackson-databind/issues/2141 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 --- .../jsr310/deser/DurationDeserializer.java | 25 +++++- .../jsr310/deser/InstantDeserializer.java | 29 ++++++- .../jsr310/TestDurationDeserialization.java | 50 ++++++++++++ .../jsr310/TestInstantSerialization.java | 79 +++++++++++++++++++ 4 files changed, 180 insertions(+), 3 deletions(-) diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/DurationDeserializer.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/DurationDeserializer.java index be0ad8bd..ef79b561 100644 --- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/DurationDeserializer.java +++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/DurationDeserializer.java @@ -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; @@ -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; @@ -40,6 +43,13 @@ public class DurationDeserializer extends JSR310DeserializerBase 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); @@ -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; } diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/InstantDeserializer.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/InstantDeserializer.java index bec6a0f1..30675f11 100644 --- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/InstantDeserializer.java +++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/InstantDeserializer.java @@ -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; @@ -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; @@ -52,6 +55,12 @@ public class InstantDeserializer { 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] * @@ -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; } @@ -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( diff --git a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/TestDurationDeserialization.java b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/TestDurationDeserialization.java index 487f24bd..fd981982 100644 --- a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/TestDurationDeserialization.java +++ b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/TestDurationDeserialization.java @@ -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; @@ -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; @@ -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 { @@ -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 { diff --git a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/TestInstantSerialization.java b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/TestInstantSerialization.java index 989934ff..ecb63b1c 100644 --- a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/TestInstantSerialization.java +++ b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/TestInstantSerialization.java @@ -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; @@ -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 {