Skip to content

Commit

Permalink
Add safe Jackson deserializers to prevent a DoS attack
Browse files Browse the repository at this point in the history
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
arteam committed Sep 30, 2018
1 parent 8781a31 commit 28c9e84
Show file tree
Hide file tree
Showing 6 changed files with 332 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ private static ObjectMapper configure(ObjectMapper mapper) {
mapper.setPropertyNamingStrategy(new AnnotationSensitivePropertyNamingStrategy());
mapper.setSubtypeResolver(new DiscoverableSubtypeResolver());

mapper.registerModule(new SafeJavaTimeModule());
return mapper;
}
}
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);
}
}
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);
}
}
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());
}
}
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;
}
}

}
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;
}
}

}

0 comments on commit 28c9e84

Please sign in to comment.