Skip to content

Commit fcfc499

Browse files
authored
Throw JsonMappingException for deeply nested JSON (#2816, CVE-2020-36518) (#3416)
1 parent 0ede935 commit fcfc499

File tree

2 files changed

+133
-47
lines changed

2 files changed

+133
-47
lines changed

src/main/java/com/fasterxml/jackson/databind/deser/std/UntypedObjectDeserializer.java

+63-47
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,10 @@ public static class Vanilla
661661
{
662662
private static final long serialVersionUID = 1L;
663663

664+
// Arbitrarily chosen.
665+
// Introduced to resolve CVE-2020-36518 and as a temporary hotfix for #2816
666+
private static final int MAX_DEPTH = 1000;
667+
664668
public final static Vanilla std = new Vanilla();
665669

666670
// @since 2.9
@@ -693,64 +697,76 @@ public Boolean supportsUpdate(DeserializationConfig config) {
693697
}
694698

695699
@Override
696-
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException
700+
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
701+
return deserialize(p, ctxt, 0);
702+
}
703+
704+
private Object deserialize(JsonParser p, DeserializationContext ctxt, int depth) throws IOException
697705
{
698706
switch (p.currentTokenId()) {
699-
case JsonTokenId.ID_START_OBJECT:
700-
{
707+
case JsonTokenId.ID_START_OBJECT: {
701708
JsonToken t = p.nextToken();
702709
if (t == JsonToken.END_OBJECT) {
703-
return new LinkedHashMap<String,Object>(2);
710+
return new LinkedHashMap<String, Object>(2);
704711
}
705712
}
706-
case JsonTokenId.ID_FIELD_NAME:
707-
return mapObject(p, ctxt);
708-
case JsonTokenId.ID_START_ARRAY:
709-
{
713+
case JsonTokenId.ID_FIELD_NAME:
714+
if (depth > MAX_DEPTH) {
715+
throw new JsonParseException(p, "JSON is too deeply nested.");
716+
}
717+
718+
return mapObject(p, ctxt, depth);
719+
case JsonTokenId.ID_START_ARRAY: {
710720
JsonToken t = p.nextToken();
711721
if (t == JsonToken.END_ARRAY) { // and empty one too
712-
if (ctxt.isEnabled(DeserializationFeature.USE_JAVA_ARRAY_FOR_JSON_ARRAY)) {
722+
if (ctxt.isEnabled(
723+
DeserializationFeature.USE_JAVA_ARRAY_FOR_JSON_ARRAY)) {
713724
return NO_OBJECTS;
714725
}
715726
return new ArrayList<Object>(2);
716727
}
717728
}
718-
if (ctxt.isEnabled(DeserializationFeature.USE_JAVA_ARRAY_FOR_JSON_ARRAY)) {
719-
return mapArrayToArray(p, ctxt);
720-
}
721-
return mapArray(p, ctxt);
722-
case JsonTokenId.ID_EMBEDDED_OBJECT:
723-
return p.getEmbeddedObject();
724-
case JsonTokenId.ID_STRING:
725-
return p.getText();
726729

727-
case JsonTokenId.ID_NUMBER_INT:
728-
if (ctxt.hasSomeOfFeatures(F_MASK_INT_COERCIONS)) {
729-
return _coerceIntegral(p, ctxt);
730+
if (depth > MAX_DEPTH) {
731+
throw new JsonParseException(p, "JSON is too deeply nested.");
730732
}
731-
return p.getNumberValue(); // should be optimal, whatever it is
732733

733-
case JsonTokenId.ID_NUMBER_FLOAT:
734-
if (ctxt.isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)) {
735-
return p.getDecimalValue();
734+
if (ctxt.isEnabled(DeserializationFeature.USE_JAVA_ARRAY_FOR_JSON_ARRAY)) {
735+
return mapArrayToArray(p, ctxt, depth);
736736
}
737-
return p.getNumberValue();
737+
return mapArray(p, ctxt, depth);
738+
case JsonTokenId.ID_EMBEDDED_OBJECT:
739+
return p.getEmbeddedObject();
740+
case JsonTokenId.ID_STRING:
741+
return p.getText();
742+
743+
case JsonTokenId.ID_NUMBER_INT:
744+
if (ctxt.hasSomeOfFeatures(F_MASK_INT_COERCIONS)) {
745+
return _coerceIntegral(p, ctxt);
746+
}
747+
return p.getNumberValue(); // should be optimal, whatever it is
738748

739-
case JsonTokenId.ID_TRUE:
740-
return Boolean.TRUE;
741-
case JsonTokenId.ID_FALSE:
742-
return Boolean.FALSE;
749+
case JsonTokenId.ID_NUMBER_FLOAT:
750+
if (ctxt.isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)) {
751+
return p.getDecimalValue();
752+
}
753+
return p.getNumberValue();
743754

744-
case JsonTokenId.ID_END_OBJECT:
745-
// 28-Oct-2015, tatu: [databind#989] We may also be given END_OBJECT (similar to FIELD_NAME),
746-
// if caller has advanced to the first token of Object, but for empty Object
747-
return new LinkedHashMap<String,Object>(2);
755+
case JsonTokenId.ID_TRUE:
756+
return Boolean.TRUE;
757+
case JsonTokenId.ID_FALSE:
758+
return Boolean.FALSE;
748759

749-
case JsonTokenId.ID_NULL: // 08-Nov-2016, tatu: yes, occurs
750-
return null;
760+
case JsonTokenId.ID_END_OBJECT:
761+
// 28-Oct-2015, tatu: [databind#989] We may also be given END_OBJECT (similar to FIELD_NAME),
762+
// if caller has advanced to the first token of Object, but for empty Object
763+
return new LinkedHashMap<String, Object>(2);
751764

752-
//case JsonTokenId.ID_END_ARRAY: // invalid
753-
default:
765+
case JsonTokenId.ID_NULL: // 08-Nov-2016, tatu: yes, occurs
766+
return null;
767+
768+
//case JsonTokenId.ID_END_ARRAY: // invalid
769+
default:
754770
}
755771
return ctxt.handleUnexpectedToken(Object.class, p);
756772
}
@@ -858,15 +874,15 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt, Object into
858874
return deserialize(p, ctxt);
859875
}
860876

861-
protected Object mapArray(JsonParser p, DeserializationContext ctxt) throws IOException
877+
protected Object mapArray(JsonParser p, DeserializationContext ctxt, int depth) throws IOException
862878
{
863-
Object value = deserialize(p, ctxt);
879+
Object value = deserialize(p, ctxt, depth + 1);
864880
if (p.nextToken() == JsonToken.END_ARRAY) {
865881
ArrayList<Object> l = new ArrayList<Object>(2);
866882
l.add(value);
867883
return l;
868884
}
869-
Object value2 = deserialize(p, ctxt);
885+
Object value2 = deserialize(p, ctxt, depth + 1);
870886
if (p.nextToken() == JsonToken.END_ARRAY) {
871887
ArrayList<Object> l = new ArrayList<Object>(2);
872888
l.add(value);
@@ -880,7 +896,7 @@ protected Object mapArray(JsonParser p, DeserializationContext ctxt) throws IOEx
880896
values[ptr++] = value2;
881897
int totalSize = ptr;
882898
do {
883-
value = deserialize(p, ctxt);
899+
value = deserialize(p, ctxt, depth + 1);
884900
++totalSize;
885901
if (ptr >= values.length) {
886902
values = buffer.appendCompletedChunk(values);
@@ -897,12 +913,12 @@ protected Object mapArray(JsonParser p, DeserializationContext ctxt) throws IOEx
897913
/**
898914
* Method called to map a JSON Array into a Java Object array (Object[]).
899915
*/
900-
protected Object[] mapArrayToArray(JsonParser p, DeserializationContext ctxt) throws IOException {
916+
protected Object[] mapArrayToArray(JsonParser p, DeserializationContext ctxt, int depth) throws IOException {
901917
ObjectBuffer buffer = ctxt.leaseObjectBuffer();
902918
Object[] values = buffer.resetAndStart();
903919
int ptr = 0;
904920
do {
905-
Object value = deserialize(p, ctxt);
921+
Object value = deserialize(p, ctxt, depth + 1);
906922
if (ptr >= values.length) {
907923
values = buffer.appendCompletedChunk(values);
908924
ptr = 0;
@@ -915,13 +931,13 @@ protected Object[] mapArrayToArray(JsonParser p, DeserializationContext ctxt) th
915931
/**
916932
* Method called to map a JSON Object into a Java value.
917933
*/
918-
protected Object mapObject(JsonParser p, DeserializationContext ctxt) throws IOException
934+
protected Object mapObject(JsonParser p, DeserializationContext ctxt, int depth) throws IOException
919935
{
920936
// will point to FIELD_NAME at this point, guaranteed
921937
// 19-Jul-2021, tatu: Was incorrectly using "getText()" before 2.13, fixed for 2.13.0
922938
String key1 = p.currentName();
923939
p.nextToken();
924-
Object value1 = deserialize(p, ctxt);
940+
Object value1 = deserialize(p, ctxt, depth + 1);
925941

926942
String key2 = p.nextFieldName();
927943
if (key2 == null) { // single entry; but we want modifiable
@@ -930,7 +946,7 @@ protected Object mapObject(JsonParser p, DeserializationContext ctxt) throws IOE
930946
return result;
931947
}
932948
p.nextToken();
933-
Object value2 = deserialize(p, ctxt);
949+
Object value2 = deserialize(p, ctxt, depth + 1);
934950

935951
String key = p.nextFieldName();
936952
if (key == null) {
@@ -952,7 +968,7 @@ protected Object mapObject(JsonParser p, DeserializationContext ctxt) throws IOE
952968

953969
do {
954970
p.nextToken();
955-
final Object newValue = deserialize(p, ctxt);
971+
final Object newValue = deserialize(p, ctxt, depth + 1);
956972
final Object oldValue = result.put(key, newValue);
957973
if (oldValue != null) {
958974
return _mapObjectWithDups(p, ctxt, result, key, oldValue, newValue,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package com.fasterxml.jackson.databind.deser;
2+
3+
import com.fasterxml.jackson.core.JsonParseException;
4+
import com.fasterxml.jackson.databind.BaseMapTest;
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import java.util.List;
7+
import java.util.Map;
8+
9+
public class DeepNestingUntypedDeserTest extends BaseMapTest
10+
{
11+
// 28-Mar-2021, tatu: Currently 3000 fails for untyped/Object,
12+
// 4000 for untyped/Array
13+
private final static int TOO_DEEP_NESTING = 4000;
14+
private final static int NOT_TOO_DEEP = 1000;
15+
16+
private final ObjectMapper MAPPER = newJsonMapper();
17+
18+
public void testTooDeepUntypedWithArray() throws Exception
19+
{
20+
final String doc = _nestedDoc(TOO_DEEP_NESTING, "[ ", "] ");
21+
try {
22+
MAPPER.readValue(doc, Object.class);
23+
fail("Should have thrown an exception.");
24+
} catch (JsonParseException jpe) {
25+
assertTrue(jpe.getMessage().startsWith("JSON is too deeply nested."));
26+
}
27+
}
28+
29+
public void testUntypedWithArray() throws Exception
30+
{
31+
final String doc = _nestedDoc(NOT_TOO_DEEP, "[ ", "] ");
32+
Object ob = MAPPER.readValue(doc, Object.class);
33+
assertTrue(ob instanceof List<?>);
34+
}
35+
36+
public void testTooDeepUntypedWithObject() throws Exception
37+
{
38+
final String doc = "{"+_nestedDoc(TOO_DEEP_NESTING, "\"x\":{", "} ") + "}";
39+
try {
40+
MAPPER.readValue(doc, Object.class);
41+
fail("Should have thrown an exception.");
42+
} catch (JsonParseException jpe) {
43+
assertTrue(jpe.getMessage().startsWith("JSON is too deeply nested."));
44+
}
45+
}
46+
47+
public void testUntypedWithObject() throws Exception
48+
{
49+
final String doc = "{"+_nestedDoc(NOT_TOO_DEEP, "\"x\":{", "} ") + "}";
50+
Object ob = MAPPER.readValue(doc, Object.class);
51+
assertTrue(ob instanceof Map<?, ?>);
52+
}
53+
54+
private String _nestedDoc(int nesting, String open, String close) {
55+
StringBuilder sb = new StringBuilder(nesting * (open.length() + close.length()));
56+
for (int i = 0; i < nesting; ++i) {
57+
sb.append(open);
58+
if ((i & 31) == 0) {
59+
sb.append("\n");
60+
}
61+
}
62+
for (int i = 0; i < nesting; ++i) {
63+
sb.append(close);
64+
if ((i & 31) == 0) {
65+
sb.append("\n");
66+
}
67+
}
68+
return sb.toString();
69+
}
70+
}

0 commit comments

Comments
 (0)