-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add safe Jackson deserializers to prevent a DoS attack
Jackson uses `BigDecimal` for deserilization of `java.time` instants and durations. The problem is that if the users sets a very big number in the scientific notation (like `1e1000000000`), it takes forever to convert `BigDecimal` to `BigInteger` to convert it to a long value. An example of the stack trace: ``` @test(timeout = 2000) public void parseBigDecimal(){ new BigDecimal("1e1000000000").longValue(); } at java.math.BigInteger.squareToomCook3(BigInteger.java:2074) at java.math.BigInteger.square(BigInteger.java:1899) at java.math.BigInteger.squareToomCook3(BigInteger.java:2053) at java.math.BigInteger.square(BigInteger.java:1899) at java.math.BigInteger.squareToomCook3(BigInteger.java:2051) at java.math.BigInteger.square(BigInteger.java:1899) at java.math.BigInteger.squareToomCook3(BigInteger.java:2049) at java.math.BigInteger.square(BigInteger.java:1899) at java.math.BigInteger.squareToomCook3(BigInteger.java:2049) at java.math.BigInteger.square(BigInteger.java:1899) at java.math.BigInteger.squareToomCook3(BigInteger.java:2055) at java.math.BigInteger.square(BigInteger.java:1899) at java.math.BigInteger.squareToomCook3(BigInteger.java:2049) at java.math.BigInteger.square(BigInteger.java:1899) at java.math.BigInteger.pow(BigInteger.java:2306) at java.math.BigDecimal.bigTenToThe(BigDecimal.java:3543) at java.math.BigDecimal.bigMultiplyPowerTen(BigDecimal.java:3676) at java.math.BigDecimal.setScale(BigDecimal.java:2445) at java.math.BigDecimal.toBigInteger(BigDecimal.java:3025) ``` A fix would be to reject big decimal values outside of the Instant and Duration ranges. See: [1] FasterXML/jackson-databind#2141 [2] https://reddit.com/r/java/comments/9jyv58/lowbandwidth_dos_vulnerability_in_jacksons/
- Loading branch information
Showing
6 changed files
with
332 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
39 changes: 39 additions & 0 deletions
39
dropwizard-jackson/src/main/java/io/dropwizard/jackson/SafeDurationDeserializer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
package io.dropwizard.jackson; | ||
|
||
import com.fasterxml.jackson.core.JsonParser; | ||
import com.fasterxml.jackson.core.JsonTokenId; | ||
import com.fasterxml.jackson.databind.DeserializationContext; | ||
import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer; | ||
import com.fasterxml.jackson.datatype.jsr310.deser.DurationDeserializer; | ||
|
||
import javax.annotation.Nullable; | ||
import java.io.IOException; | ||
import java.math.BigDecimal; | ||
import java.time.Duration; | ||
|
||
/** | ||
* Safe deserializer for `Instant` that rejects big decimal values out of the range of Long. | ||
* They take forever to deserialize and can be used in a DoS attack. | ||
*/ | ||
class SafeDurationDeserializer extends StdScalarDeserializer<Duration> { | ||
|
||
private static final BigDecimal MAX_DURATION = new BigDecimal(Long.MAX_VALUE); | ||
private static final BigDecimal MIN_DURATION = new BigDecimal(Long.MIN_VALUE); | ||
|
||
SafeDurationDeserializer() { | ||
super(Duration.class); | ||
} | ||
|
||
@Override | ||
@Nullable | ||
public Duration deserialize(JsonParser parser, DeserializationContext context) throws IOException { | ||
if (parser.getCurrentTokenId() == JsonTokenId.ID_NUMBER_FLOAT) { | ||
BigDecimal value = parser.getDecimalValue(); | ||
// new BigDecimal("1e1000000000").longValue() takes forever to complete | ||
if (value.compareTo(MAX_DURATION) > 0 || value.compareTo(MIN_DURATION) < 0) { | ||
throw new IllegalArgumentException("Value is out of range of Duration"); | ||
} | ||
} | ||
return DurationDeserializer.INSTANCE.deserialize(parser, context); | ||
} | ||
} |
43 changes: 43 additions & 0 deletions
43
dropwizard-jackson/src/main/java/io/dropwizard/jackson/SafeInstantDeserializer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package io.dropwizard.jackson; | ||
|
||
import com.fasterxml.jackson.databind.DeserializationContext; | ||
import com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer; | ||
|
||
import javax.annotation.Nullable; | ||
import java.math.BigDecimal; | ||
import java.time.Instant; | ||
import java.time.ZoneId; | ||
import java.time.format.DateTimeFormatter; | ||
import java.time.temporal.Temporal; | ||
import java.time.temporal.TemporalAccessor; | ||
import java.util.function.BiFunction; | ||
import java.util.function.Function; | ||
|
||
/** | ||
* Safe deserializer for `Instant` that rejects big decimal values that take forever to deserialize | ||
* and can be used in a DoS attack. | ||
*/ | ||
class SafeInstantDeserializer<T extends Temporal> extends InstantDeserializer<T> { | ||
|
||
private static final BigDecimal MAX_INSTANT = new BigDecimal(Instant.MAX.getEpochSecond() + 1); | ||
private static final BigDecimal MIN_INSTANT = new BigDecimal(Instant.MIN.getEpochSecond()); | ||
|
||
SafeInstantDeserializer(Class<T> supportedType, | ||
DateTimeFormatter formatter, | ||
Function<TemporalAccessor, T> parsedToValue, | ||
Function<FromIntegerArguments, T> fromMilliseconds, | ||
Function<FromDecimalArguments, T> fromNanoseconds, | ||
@Nullable BiFunction<T, ZoneId, T> adjust, | ||
boolean replaceZeroOffsetAsZ) { | ||
super(supportedType, formatter, parsedToValue, fromMilliseconds, fromNanoseconds, adjust, replaceZeroOffsetAsZ); | ||
} | ||
|
||
@Override | ||
protected T _fromDecimal(DeserializationContext context, BigDecimal value) { | ||
// new BigDecimal("1e1000000000").longValue() takes forever to complete | ||
if (value.compareTo(MAX_INSTANT) >= 0 || value.compareTo(MIN_INSTANT) < 0) { | ||
throw new IllegalArgumentException("Value is out of range of Instant"); | ||
} | ||
return super._fromDecimal(context, value); | ||
} | ||
} |
53 changes: 53 additions & 0 deletions
53
dropwizard-jackson/src/main/java/io/dropwizard/jackson/SafeJavaTimeModule.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package io.dropwizard.jackson; | ||
|
||
import com.fasterxml.jackson.databind.module.SimpleModule; | ||
import com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer; | ||
import com.fasterxml.jackson.module.paramnames.PackageVersion; | ||
|
||
import java.time.Duration; | ||
import java.time.Instant; | ||
import java.time.OffsetDateTime; | ||
import java.time.ZonedDateTime; | ||
import java.time.format.DateTimeFormatter; | ||
|
||
/** | ||
* Module that provides safe deserializers for Instant and Duration that reject big decimal values | ||
* outside of their range which are extremely CPU-heavy to parse. | ||
*/ | ||
class SafeJavaTimeModule extends SimpleModule { | ||
|
||
private static final InstantDeserializer<Instant> INSTANT = new SafeInstantDeserializer<>( | ||
Instant.class, DateTimeFormatter.ISO_INSTANT, | ||
Instant::from, | ||
a -> Instant.ofEpochMilli(a.value), | ||
a -> Instant.ofEpochSecond(a.integer, a.fraction), | ||
null, | ||
true | ||
); | ||
|
||
private static final InstantDeserializer<OffsetDateTime> OFFSET_DATE_TIME = new SafeInstantDeserializer<>( | ||
OffsetDateTime.class, DateTimeFormatter.ISO_OFFSET_DATE_TIME, | ||
OffsetDateTime::from, | ||
a -> OffsetDateTime.ofInstant(Instant.ofEpochMilli(a.value), a.zoneId), | ||
a -> OffsetDateTime.ofInstant(Instant.ofEpochSecond(a.integer, a.fraction), a.zoneId), | ||
(d, z) -> d.withOffsetSameInstant(z.getRules().getOffset(d.toLocalDateTime())), | ||
true | ||
); | ||
|
||
private static final InstantDeserializer<ZonedDateTime> ZONED_DATE_TIME = new SafeInstantDeserializer<>( | ||
ZonedDateTime.class, DateTimeFormatter.ISO_ZONED_DATE_TIME, | ||
ZonedDateTime::from, | ||
a -> ZonedDateTime.ofInstant(Instant.ofEpochMilli(a.value), a.zoneId), | ||
a -> ZonedDateTime.ofInstant(Instant.ofEpochSecond(a.integer, a.fraction), a.zoneId), | ||
ZonedDateTime::withZoneSameInstant, | ||
false | ||
); | ||
|
||
SafeJavaTimeModule() { | ||
super(PackageVersion.VERSION); | ||
addDeserializer(Instant.class, INSTANT); | ||
addDeserializer(OffsetDateTime.class, OFFSET_DATE_TIME); | ||
addDeserializer(ZonedDateTime.class, ZONED_DATE_TIME); | ||
addDeserializer(Duration.class, new SafeDurationDeserializer()); | ||
} | ||
} |
101 changes: 101 additions & 0 deletions
101
...src/test/java/io/dropwizard/jackson/JacksonDeserializationOfBigNumbersToDurationTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
package io.dropwizard.jackson; | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import com.fasterxml.jackson.databind.JsonMappingException; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import org.junit.Test; | ||
|
||
import javax.annotation.Nullable; | ||
import java.time.Duration; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; | ||
|
||
public class JacksonDeserializationOfBigNumbersToDurationTest { | ||
|
||
private final ObjectMapper objectMapper = Jackson.newObjectMapper(); | ||
|
||
@Test(timeout = 5000) | ||
public void testDoesNotAttemptToDeserializeExtremelyBigNumbers() { | ||
assertThatExceptionOfType(JsonMappingException.class).isThrownBy( | ||
() -> objectMapper.readValue("{\"id\": 42, \"duration\": 1e1000000000}", Task.class)) | ||
.withMessageStartingWith("Value is out of range of Duration"); | ||
} | ||
|
||
@Test | ||
public void testCanDeserializeZero() throws Exception { | ||
Task task = objectMapper.readValue("{\"id\": 42, \"duration\": 0}", Task.class); | ||
assertThat(task.getDuration()).isEqualTo(Duration.ofSeconds(0)); | ||
} | ||
|
||
@Test | ||
public void testCanDeserializeNormalTimestamp() throws Exception { | ||
Task task = objectMapper.readValue("{\"id\": 42, \"duration\": 30}", Task.class); | ||
assertThat(task.getDuration()).isEqualTo(Duration.ofSeconds(30)); | ||
} | ||
|
||
@Test | ||
public void testCanDeserializeNormalTimestampWithNanoseconds() throws Exception { | ||
Task task = objectMapper.readValue("{\"id\": 42, \"duration\": 30.314400507}", Task.class); | ||
assertThat(task.getDuration()).isEqualTo(Duration.ofSeconds(30, 314400507L)); | ||
} | ||
|
||
@Test | ||
public void testCanDeserializeFromString() throws Exception { | ||
Task task = objectMapper.readValue("{\"id\": 42, \"duration\": \"PT30S\"}", Task.class); | ||
assertThat(task.getDuration()).isEqualTo(Duration.ofSeconds(30)); | ||
} | ||
|
||
@Test | ||
public void testCanDeserializeMinDuration() throws Exception { | ||
Task task = objectMapper.readValue("{\"id\": 42, \"duration\": -9223372036854775808}", Task.class); | ||
assertThat(task.getDuration()).isEqualTo(Duration.ofSeconds(Long.MIN_VALUE)); | ||
} | ||
|
||
@Test | ||
public void testCanDeserializeMaxDuration() throws Exception { | ||
Task task = objectMapper.readValue("{\"id\": 42, \"duration\": 9223372036854775807}", Task.class); | ||
assertThat(task.getDuration()).isEqualTo(Duration.ofSeconds(Long.MAX_VALUE)); | ||
} | ||
|
||
@Test | ||
public void testCanNotDeserializeValueMoreThanMaxDuration() { | ||
assertThatExceptionOfType(JsonMappingException.class).isThrownBy( | ||
() -> objectMapper.readValue("{\"id\": 42, \"duration\": 9223372036854775808}", Task.class)); | ||
} | ||
|
||
@Test | ||
public void testCanNotDeserializeValueLessThanMinDuration() { | ||
assertThatExceptionOfType(JsonMappingException.class).isThrownBy( | ||
() -> objectMapper.readValue("{\"id\": 42, \"duration\": -9223372036854775809}", Task.class)); | ||
} | ||
|
||
static class Task { | ||
|
||
private int id; | ||
@Nullable | ||
private Duration duration; | ||
|
||
@JsonProperty | ||
int getId() { | ||
return id; | ||
} | ||
|
||
@JsonProperty | ||
void setId(int id) { | ||
this.id = id; | ||
} | ||
|
||
@JsonProperty | ||
@Nullable | ||
Duration getDuration() { | ||
return duration; | ||
} | ||
|
||
@JsonProperty | ||
void setDuration(Duration duration) { | ||
this.duration = duration; | ||
} | ||
} | ||
|
||
} |
95 changes: 95 additions & 0 deletions
95
.../src/test/java/io/dropwizard/jackson/JacksonDeserializationOfBigNumbersToInstantTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
package io.dropwizard.jackson; | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import com.fasterxml.jackson.databind.JsonMappingException; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import org.junit.Test; | ||
|
||
import javax.annotation.Nullable; | ||
import java.time.Instant; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; | ||
|
||
public class JacksonDeserializationOfBigNumbersToInstantTest { | ||
|
||
private final ObjectMapper objectMapper = Jackson.newObjectMapper(); | ||
|
||
@Test(timeout = 5000) | ||
public void testDoesNotAttemptToDeserializeExtremelBigNumbers() { | ||
assertThatExceptionOfType(JsonMappingException.class).isThrownBy( | ||
() -> objectMapper.readValue("{\"id\": 42, \"createdAt\": 1e1000000000}", Event.class)) | ||
.withMessageStartingWith("Value is out of range of Instant"); | ||
} | ||
|
||
@Test | ||
public void testCanDeserializeZero() throws Exception { | ||
Event event = objectMapper.readValue("{\"id\": 42, \"createdAt\": 0}", Event.class); | ||
assertThat(event.getCreatedAt()).isEqualTo(Instant.ofEpochMilli(0)); | ||
} | ||
|
||
@Test | ||
public void testCanDeserializeNormalTimestamp() throws Exception { | ||
Event event = objectMapper.readValue("{\"id\": 42, \"createdAt\": 1538326357}", Event.class); | ||
assertThat(event.getCreatedAt()).isEqualTo(Instant.ofEpochMilli(1538326357000L)); | ||
} | ||
|
||
@Test | ||
public void testCanDeserializeNormalTimestampWithNanoseconds() throws Exception { | ||
Event event = objectMapper.readValue("{\"id\": 42, \"createdAt\": 1538326357.298509112}", Event.class); | ||
assertThat(event.getCreatedAt()).isEqualTo(Instant.ofEpochSecond(1538326357, 298509112L)); | ||
} | ||
|
||
@Test | ||
public void testCanDeserializeMinInstant() throws Exception { | ||
Event event = objectMapper.readValue("{\"id\": 42, \"createdAt\": -31557014167219200}", Event.class); | ||
assertThat(event.getCreatedAt()).isEqualTo(Instant.MIN); | ||
} | ||
|
||
@Test | ||
public void testCanDeserializeMaxInstant() throws Exception { | ||
Event event = objectMapper.readValue("{\"id\": 42, \"createdAt\": 31556889864403199.999999999}", Event.class); | ||
assertThat(event.getCreatedAt()).isEqualTo(Instant.MAX); | ||
} | ||
|
||
@Test | ||
public void testCanNotDeserializeValueMoreThanMaxInstant() { | ||
assertThatExceptionOfType(JsonMappingException.class).isThrownBy( | ||
() -> objectMapper.readValue("{\"id\": 42, \"createdAt\": 31556889864403200}", Event.class)); | ||
} | ||
|
||
@Test | ||
public void testCanNotDeserializeValueLessThanMaxInstant() { | ||
assertThatExceptionOfType(JsonMappingException.class).isThrownBy( | ||
() -> objectMapper.readValue("{\"id\": 42, \"createdAt\": -31557014167219201}", Event.class)); | ||
} | ||
|
||
static class Event { | ||
|
||
private int id; | ||
@Nullable | ||
private Instant createdAt; | ||
|
||
@JsonProperty | ||
int getId() { | ||
return id; | ||
} | ||
|
||
@JsonProperty | ||
void setId(int id) { | ||
this.id = id; | ||
} | ||
|
||
@JsonProperty | ||
@Nullable | ||
Instant getCreatedAt() { | ||
return createdAt; | ||
} | ||
|
||
@JsonProperty | ||
void setCreatedAt(Instant createdAt) { | ||
this.createdAt = createdAt; | ||
} | ||
} | ||
|
||
} |