Skip to content

Commit 824c49a

Browse files
author
Luca Di Grazia
committed
Add safe Jackson deserializers to prevent a DoS attack (#2511)
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/
1 parent 2d05157 commit 824c49a

File tree

8 files changed

+170
-59
lines changed

8 files changed

+170
-59
lines changed

dataset/GitHub_Java/dropwizard.dropwizard/dropwizard-core/src/main/java/io/dropwizard/cli/EnvironmentCommand.java

+6-22
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
import io.dropwizard.setup.Environment;
77
import net.sourceforge.argparse4j.inf.Namespace;
88

9-
import javax.annotation.Nullable;
10-
119
/**
1210
* A command which executes with a configured {@link Environment}.
1311
*
@@ -16,8 +14,6 @@
1614
*/
1715
public abstract class EnvironmentCommand<T extends Configuration> extends ConfiguredCommand<T> {
1816
private final Application<T> application;
19-
@Nullable
20-
private Environment environment;
2117

2218
/**
2319
* Creates a new environment command.
@@ -31,26 +27,14 @@ protected EnvironmentCommand(Application<T> application, String name, String des
3127
this.application = application;
3228
}
3329

34-
/**
35-
* Returns the constructed environment or {@code null} if it hasn't been constructed yet.
36-
*
37-
* @return Returns the constructed environment or {@code null} if it hasn't been constructed yet
38-
* @since 2.0.19
39-
*/
40-
@Nullable
41-
public Environment getEnvironment() {
42-
return environment;
43-
}
44-
4530
@Override
4631
protected void run(Bootstrap<T> bootstrap, Namespace namespace, T configuration) throws Exception {
47-
this.environment = new Environment(bootstrap.getApplication().getName(),
48-
bootstrap.getObjectMapper(),
49-
bootstrap.getValidatorFactory(),
50-
bootstrap.getMetricRegistry(),
51-
bootstrap.getClassLoader(),
52-
bootstrap.getHealthCheckRegistry(),
53-
configuration);
32+
final Environment environment = new Environment(bootstrap.getApplication().getName(),
33+
bootstrap.getObjectMapper(),
34+
bootstrap.getValidatorFactory(),
35+
bootstrap.getMetricRegistry(),
36+
bootstrap.getClassLoader(),
37+
bootstrap.getHealthCheckRegistry());
5438
configuration.getMetricsFactory().configure(environment.lifecycle(),
5539
bootstrap.getMetricRegistry());
5640
configuration.getServerFactory().configure(environment);

dataset/GitHub_Java/dropwizard.dropwizard/dropwizard-core/src/main/java/io/dropwizard/setup/Environment.java

+1-3
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public Environment(String name,
7676

7777
this.adminEnvironment = new AdminEnvironment(adminContext, healthCheckRegistry, metricRegistry);
7878

79-
this.lifecycleEnvironment = new LifecycleEnvironment(metricRegistry);
79+
this.lifecycleEnvironment = new LifecycleEnvironment();
8080

8181
final DropwizardResourceConfig jerseyConfig = new DropwizardResourceConfig(metricRegistry);
8282
jerseyConfig.setContextPath(servletContext.getContextPath());
@@ -113,8 +113,6 @@ public Environment(String name,
113113

114114
/**
115115
* Creates an environment and enables injecting validator feature.
116-
*
117-
* @since 2.0
118116
*/
119117
public Environment(String name,
120118
ObjectMapper objectMapper,

dataset/GitHub_Java/dropwizard.dropwizard/dropwizard-jackson/src/main/java/io/dropwizard/jackson/Jackson.java

+1-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package io.dropwizard.jackson;
22

3-
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
4-
53
import com.fasterxml.jackson.core.JsonFactory;
64
import com.fasterxml.jackson.databind.ObjectMapper;
75
import com.fasterxml.jackson.datatype.guava.GuavaModule;
@@ -51,8 +49,7 @@ public static ObjectMapper newObjectMapper(@Nullable JsonFactory jsonFactory) {
5149
public static ObjectMapper newMinimalObjectMapper() {
5250
return new ObjectMapper()
5351
.registerModule(new GuavaModule())
54-
.setSubtypeResolver(new DiscoverableSubtypeResolver())
55-
.disable(FAIL_ON_UNKNOWN_PROPERTIES);
52+
.setSubtypeResolver(new DiscoverableSubtypeResolver());
5653
}
5754

5855
private static ObjectMapper configure(ObjectMapper mapper) {
@@ -67,7 +64,6 @@ private static ObjectMapper configure(ObjectMapper mapper) {
6764
mapper.registerModule(new JavaTimeModule());
6865
mapper.setPropertyNamingStrategy(new AnnotationSensitivePropertyNamingStrategy());
6966
mapper.setSubtypeResolver(new DiscoverableSubtypeResolver());
70-
mapper.disable(FAIL_ON_UNKNOWN_PROPERTIES);
7167

7268
mapper.registerModule(new SafeJavaTimeModule());
7369
return mapper;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package io.dropwizard.jackson;
2+
3+
import com.fasterxml.jackson.core.JsonParser;
4+
import com.fasterxml.jackson.core.JsonTokenId;
5+
import com.fasterxml.jackson.databind.DeserializationContext;
6+
import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer;
7+
import com.fasterxml.jackson.datatype.jsr310.deser.DurationDeserializer;
8+
9+
import javax.annotation.Nullable;
10+
import java.io.IOException;
11+
import java.math.BigDecimal;
12+
import java.time.Duration;
13+
14+
/**
15+
* Safe deserializer for `Instant` that rejects big decimal values out of the range of Long.
16+
* They take forever to deserialize and can be used in a DoS attack.
17+
*/
18+
class SafeDurationDeserializer extends StdScalarDeserializer<Duration> {
19+
20+
private static final BigDecimal MAX_DURATION = new BigDecimal(Long.MAX_VALUE);
21+
private static final BigDecimal MIN_DURATION = new BigDecimal(Long.MIN_VALUE);
22+
23+
SafeDurationDeserializer() {
24+
super(Duration.class);
25+
}
26+
27+
@Override
28+
@Nullable
29+
public Duration deserialize(JsonParser parser, DeserializationContext context) throws IOException {
30+
if (parser.getCurrentTokenId() == JsonTokenId.ID_NUMBER_FLOAT) {
31+
BigDecimal value = parser.getDecimalValue();
32+
// new BigDecimal("1e1000000000").longValue() takes forever to complete
33+
if (value.compareTo(MAX_DURATION) > 0 || value.compareTo(MIN_DURATION) < 0) {
34+
throw new IllegalArgumentException("Value is out of range of Duration");
35+
}
36+
}
37+
return DurationDeserializer.INSTANCE.deserialize(parser, context);
38+
}
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package io.dropwizard.jackson;
2+
3+
import com.fasterxml.jackson.databind.DeserializationContext;
4+
import com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer;
5+
6+
import javax.annotation.Nullable;
7+
import java.math.BigDecimal;
8+
import java.time.Instant;
9+
import java.time.ZoneId;
10+
import java.time.format.DateTimeFormatter;
11+
import java.time.temporal.Temporal;
12+
import java.time.temporal.TemporalAccessor;
13+
import java.util.function.BiFunction;
14+
import java.util.function.Function;
15+
16+
/**
17+
* Safe deserializer for `Instant` that rejects big decimal values that take forever to deserialize
18+
* and can be used in a DoS attack.
19+
*/
20+
class SafeInstantDeserializer<T extends Temporal> extends InstantDeserializer<T> {
21+
22+
private static final BigDecimal MAX_INSTANT = new BigDecimal(Instant.MAX.getEpochSecond() + 1);
23+
private static final BigDecimal MIN_INSTANT = new BigDecimal(Instant.MIN.getEpochSecond());
24+
25+
SafeInstantDeserializer(Class<T> supportedType,
26+
DateTimeFormatter formatter,
27+
Function<TemporalAccessor, T> parsedToValue,
28+
Function<FromIntegerArguments, T> fromMilliseconds,
29+
Function<FromDecimalArguments, T> fromNanoseconds,
30+
@Nullable BiFunction<T, ZoneId, T> adjust,
31+
boolean replaceZeroOffsetAsZ) {
32+
super(supportedType, formatter, parsedToValue, fromMilliseconds, fromNanoseconds, adjust, replaceZeroOffsetAsZ);
33+
}
34+
35+
@Override
36+
protected T _fromDecimal(DeserializationContext context, BigDecimal value) {
37+
// new BigDecimal("1e1000000000").longValue() takes forever to complete
38+
if (value.compareTo(MAX_INSTANT) >= 0 || value.compareTo(MIN_INSTANT) < 0) {
39+
throw new IllegalArgumentException("Value is out of range of Instant");
40+
}
41+
return super._fromDecimal(context, value);
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package io.dropwizard.jackson;
2+
3+
import com.fasterxml.jackson.databind.module.SimpleModule;
4+
import com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer;
5+
import com.fasterxml.jackson.module.paramnames.PackageVersion;
6+
7+
import java.time.Duration;
8+
import java.time.Instant;
9+
import java.time.OffsetDateTime;
10+
import java.time.ZonedDateTime;
11+
import java.time.format.DateTimeFormatter;
12+
13+
/**
14+
* Module that provides safe deserializers for Instant and Duration that reject big decimal values
15+
* outside of their range which are extremely CPU-heavy to parse.
16+
*/
17+
class SafeJavaTimeModule extends SimpleModule {
18+
19+
private static final InstantDeserializer<Instant> INSTANT = new SafeInstantDeserializer<>(
20+
Instant.class, DateTimeFormatter.ISO_INSTANT,
21+
Instant::from,
22+
a -> Instant.ofEpochMilli(a.value),
23+
a -> Instant.ofEpochSecond(a.integer, a.fraction),
24+
null,
25+
true
26+
);
27+
28+
private static final InstantDeserializer<OffsetDateTime> OFFSET_DATE_TIME = new SafeInstantDeserializer<>(
29+
OffsetDateTime.class, DateTimeFormatter.ISO_OFFSET_DATE_TIME,
30+
OffsetDateTime::from,
31+
a -> OffsetDateTime.ofInstant(Instant.ofEpochMilli(a.value), a.zoneId),
32+
a -> OffsetDateTime.ofInstant(Instant.ofEpochSecond(a.integer, a.fraction), a.zoneId),
33+
(d, z) -> d.withOffsetSameInstant(z.getRules().getOffset(d.toLocalDateTime())),
34+
true
35+
);
36+
37+
private static final InstantDeserializer<ZonedDateTime> ZONED_DATE_TIME = new SafeInstantDeserializer<>(
38+
ZonedDateTime.class, DateTimeFormatter.ISO_ZONED_DATE_TIME,
39+
ZonedDateTime::from,
40+
a -> ZonedDateTime.ofInstant(Instant.ofEpochMilli(a.value), a.zoneId),
41+
a -> ZonedDateTime.ofInstant(Instant.ofEpochSecond(a.integer, a.fraction), a.zoneId),
42+
ZonedDateTime::withZoneSameInstant,
43+
false
44+
);
45+
46+
SafeJavaTimeModule() {
47+
super(PackageVersion.VERSION);
48+
addDeserializer(Instant.class, INSTANT);
49+
addDeserializer(OffsetDateTime.class, OFFSET_DATE_TIME);
50+
addDeserializer(ZonedDateTime.class, ZONED_DATE_TIME);
51+
addDeserializer(Duration.class, new SafeDurationDeserializer());
52+
}
53+
}

dataset/GitHub_Java/dropwizard.dropwizard/dropwizard-jackson/src/test/java/io/dropwizard/jackson/JacksonDeserializationOfBigNumbersToDurationTest.java

+14-15
Original file line numberDiff line numberDiff line change
@@ -3,70 +3,69 @@
33
import com.fasterxml.jackson.annotation.JsonProperty;
44
import com.fasterxml.jackson.databind.JsonMappingException;
55
import com.fasterxml.jackson.databind.ObjectMapper;
6-
import org.junit.jupiter.api.Test;
6+
import org.junit.Test;
77

88
import javax.annotation.Nullable;
99
import java.time.Duration;
1010

1111
import static org.assertj.core.api.Assertions.assertThat;
1212
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
13-
import static org.junit.jupiter.api.Assertions.assertTimeout;
14-
1513

1614
public class JacksonDeserializationOfBigNumbersToDurationTest {
1715

1816
private final ObjectMapper objectMapper = Jackson.newObjectMapper();
1917

20-
@Test
21-
void testDoesNotAttemptToDeserializeExtremelyBigNumbers() throws Exception {
22-
Task task = objectMapper.readValue("{\"id\": 42, \"duration\": 1e1000000000}", Task.class);
23-
assertTimeout(Duration.ofSeconds(5L), () -> assertThat(task.getDuration()).isEqualTo(Duration.ofSeconds(0)));
18+
@Test(timeout = 5000)
19+
public void testDoesNotAttemptToDeserializeExtremelyBigNumbers() {
20+
assertThatExceptionOfType(JsonMappingException.class).isThrownBy(
21+
() -> objectMapper.readValue("{\"id\": 42, \"duration\": 1e1000000000}", Task.class))
22+
.withMessageStartingWith("Value is out of range of Duration");
2423
}
2524

2625
@Test
27-
void testCanDeserializeZero() throws Exception {
26+
public void testCanDeserializeZero() throws Exception {
2827
Task task = objectMapper.readValue("{\"id\": 42, \"duration\": 0}", Task.class);
2928
assertThat(task.getDuration()).isEqualTo(Duration.ofSeconds(0));
3029
}
3130

3231
@Test
33-
void testCanDeserializeNormalTimestamp() throws Exception {
32+
public void testCanDeserializeNormalTimestamp() throws Exception {
3433
Task task = objectMapper.readValue("{\"id\": 42, \"duration\": 30}", Task.class);
3534
assertThat(task.getDuration()).isEqualTo(Duration.ofSeconds(30));
3635
}
3736

3837
@Test
39-
void testCanDeserializeNormalTimestampWithNanoseconds() throws Exception {
38+
public void testCanDeserializeNormalTimestampWithNanoseconds() throws Exception {
4039
Task task = objectMapper.readValue("{\"id\": 42, \"duration\": 30.314400507}", Task.class);
4140
assertThat(task.getDuration()).isEqualTo(Duration.ofSeconds(30, 314400507L));
4241
}
4342

4443
@Test
45-
void testCanDeserializeFromString() throws Exception {
44+
public void testCanDeserializeFromString() throws Exception {
4645
Task task = objectMapper.readValue("{\"id\": 42, \"duration\": \"PT30S\"}", Task.class);
4746
assertThat(task.getDuration()).isEqualTo(Duration.ofSeconds(30));
4847
}
4948

5049
@Test
51-
void testCanDeserializeMinDuration() throws Exception {
50+
public void testCanDeserializeMinDuration() throws Exception {
5251
Task task = objectMapper.readValue("{\"id\": 42, \"duration\": -9223372036854775808}", Task.class);
5352
assertThat(task.getDuration()).isEqualTo(Duration.ofSeconds(Long.MIN_VALUE));
5453
}
5554

5655
@Test
57-
void testCanDeserializeMaxDuration() throws Exception {
56+
public void testCanDeserializeMaxDuration() throws Exception {
5857
Task task = objectMapper.readValue("{\"id\": 42, \"duration\": 9223372036854775807}", Task.class);
5958
assertThat(task.getDuration()).isEqualTo(Duration.ofSeconds(Long.MAX_VALUE));
6059
}
6160

6261
@Test
63-
void testCanNotDeserializeValueMoreThanMaxDuration() {
62+
public void testCanNotDeserializeValueMoreThanMaxDuration() {
6463
assertThatExceptionOfType(JsonMappingException.class).isThrownBy(
6564
() -> objectMapper.readValue("{\"id\": 42, \"duration\": 9223372036854775808}", Task.class));
6665
}
6766

6867
@Test
69-
void testCanNotDeserializeValueLessThanMinDuration() {
68+
public void testCanNotDeserializeValueLessThanMinDuration() {
7069
assertThatExceptionOfType(JsonMappingException.class).isThrownBy(
7170
() -> objectMapper.readValue("{\"id\": 42, \"duration\": -9223372036854775809}", Task.class));
7271
}

dataset/GitHub_Java/dropwizard.dropwizard/dropwizard-jackson/src/test/java/io/dropwizard/jackson/JacksonDeserializationOfBigNumbersToInstantTest.java

+13-14
Original file line numberDiff line numberDiff line change
@@ -3,64 +3,63 @@
33
import com.fasterxml.jackson.annotation.JsonProperty;
44
import com.fasterxml.jackson.databind.JsonMappingException;
55
import com.fasterxml.jackson.databind.ObjectMapper;
6-
import org.junit.jupiter.api.Test;
6+
import org.junit.Test;
77

88
import javax.annotation.Nullable;
9-
import java.time.Duration;
109
import java.time.Instant;
1110

1211
import static org.assertj.core.api.Assertions.assertThat;
1312
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
14-
import static org.junit.jupiter.api.Assertions.assertTimeout;
1513

1614
public class JacksonDeserializationOfBigNumbersToInstantTest {
1715

1816
private final ObjectMapper objectMapper = Jackson.newObjectMapper();
1917

20-
@Test
21-
void testDoesNotAttemptToDeserializeExtremelBigNumbers() throws Exception {
22-
Event event = objectMapper.readValue("{\"id\": 42, \"createdAt\": 1e1000000000}", Event.class);
23-
assertTimeout(Duration.ofSeconds(5L), () -> assertThat(event.getCreatedAt()).isEqualTo(Instant.ofEpochMilli(0)));
18+
@Test(timeout = 5000)
19+
public void testDoesNotAttemptToDeserializeExtremelBigNumbers() {
20+
assertThatExceptionOfType(JsonMappingException.class).isThrownBy(
21+
() -> objectMapper.readValue("{\"id\": 42, \"createdAt\": 1e1000000000}", Event.class))
22+
.withMessageStartingWith("Value is out of range of Instant");
2423
}
2524

2625
@Test
27-
void testCanDeserializeZero() throws Exception {
26+
public void testCanDeserializeZero() throws Exception {
2827
Event event = objectMapper.readValue("{\"id\": 42, \"createdAt\": 0}", Event.class);
2928
assertThat(event.getCreatedAt()).isEqualTo(Instant.ofEpochMilli(0));
3029
}
3130

3231
@Test
33-
void testCanDeserializeNormalTimestamp() throws Exception {
32+
public void testCanDeserializeNormalTimestamp() throws Exception {
3433
Event event = objectMapper.readValue("{\"id\": 42, \"createdAt\": 1538326357}", Event.class);
3534
assertThat(event.getCreatedAt()).isEqualTo(Instant.ofEpochMilli(1538326357000L));
3635
}
3736

3837
@Test
39-
void testCanDeserializeNormalTimestampWithNanoseconds() throws Exception {
38+
public void testCanDeserializeNormalTimestampWithNanoseconds() throws Exception {
4039
Event event = objectMapper.readValue("{\"id\": 42, \"createdAt\": 1538326357.298509112}", Event.class);
4140
assertThat(event.getCreatedAt()).isEqualTo(Instant.ofEpochSecond(1538326357, 298509112L));
4241
}
4342

4443
@Test
45-
void testCanDeserializeMinInstant() throws Exception {
44+
public void testCanDeserializeMinInstant() throws Exception {
4645
Event event = objectMapper.readValue("{\"id\": 42, \"createdAt\": -31557014167219200}", Event.class);
4746
assertThat(event.getCreatedAt()).isEqualTo(Instant.MIN);
4847
}
4948

5049
@Test
51-
void testCanDeserializeMaxInstant() throws Exception {
50+
public void testCanDeserializeMaxInstant() throws Exception {
5251
Event event = objectMapper.readValue("{\"id\": 42, \"createdAt\": 31556889864403199.999999999}", Event.class);
5352
assertThat(event.getCreatedAt()).isEqualTo(Instant.MAX);
5453
}
5554

5655
@Test
57-
void testCanNotDeserializeValueMoreThanMaxInstant() {
56+
public void testCanNotDeserializeValueMoreThanMaxInstant() {
5857
assertThatExceptionOfType(JsonMappingException.class).isThrownBy(
5958
() -> objectMapper.readValue("{\"id\": 42, \"createdAt\": 31556889864403200}", Event.class));
6059
}
6160

6261
@Test
63-
void testCanNotDeserializeValueLessThanMaxInstant() {
62+
public void testCanNotDeserializeValueLessThanMaxInstant() {
6463
assertThatExceptionOfType(JsonMappingException.class).isThrownBy(
6564
() -> objectMapper.readValue("{\"id\": 42, \"createdAt\": -31557014167219201}", Event.class));
6665
}

0 commit comments

Comments
 (0)