Skip to content

Commit

Permalink
Throw JsonMappingException for deeply nested JSON (#2816, CVE-2020-36518
Browse files Browse the repository at this point in the history
) (#3416)
  • Loading branch information
TaylorSMarks authored Mar 22, 2022
1 parent 0ede935 commit fcfc499
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,10 @@ public static class Vanilla
{
private static final long serialVersionUID = 1L;

// Arbitrarily chosen.
// Introduced to resolve CVE-2020-36518 and as a temporary hotfix for #2816
private static final int MAX_DEPTH = 1000;

public final static Vanilla std = new Vanilla();

// @since 2.9
Expand Down Expand Up @@ -693,64 +697,76 @@ public Boolean supportsUpdate(DeserializationConfig config) {
}

@Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return deserialize(p, ctxt, 0);
}

private Object deserialize(JsonParser p, DeserializationContext ctxt, int depth) throws IOException
{
switch (p.currentTokenId()) {
case JsonTokenId.ID_START_OBJECT:
{
case JsonTokenId.ID_START_OBJECT: {
JsonToken t = p.nextToken();
if (t == JsonToken.END_OBJECT) {
return new LinkedHashMap<String,Object>(2);
return new LinkedHashMap<String, Object>(2);
}
}
case JsonTokenId.ID_FIELD_NAME:
return mapObject(p, ctxt);
case JsonTokenId.ID_START_ARRAY:
{
case JsonTokenId.ID_FIELD_NAME:

This comment has been minimized.

Copy link
@stickycode

stickycode Mar 25, 2022

This formatting really obscures the actual change being made, I would suggest cleaning this up before doing the 2.13.3 release.

if (depth > MAX_DEPTH) {
throw new JsonParseException(p, "JSON is too deeply nested.");
}

return mapObject(p, ctxt, depth);
case JsonTokenId.ID_START_ARRAY: {
JsonToken t = p.nextToken();
if (t == JsonToken.END_ARRAY) { // and empty one too
if (ctxt.isEnabled(DeserializationFeature.USE_JAVA_ARRAY_FOR_JSON_ARRAY)) {
if (ctxt.isEnabled(
DeserializationFeature.USE_JAVA_ARRAY_FOR_JSON_ARRAY)) {
return NO_OBJECTS;
}
return new ArrayList<Object>(2);
}
}
if (ctxt.isEnabled(DeserializationFeature.USE_JAVA_ARRAY_FOR_JSON_ARRAY)) {
return mapArrayToArray(p, ctxt);
}
return mapArray(p, ctxt);
case JsonTokenId.ID_EMBEDDED_OBJECT:
return p.getEmbeddedObject();
case JsonTokenId.ID_STRING:
return p.getText();

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

case JsonTokenId.ID_NUMBER_FLOAT:
if (ctxt.isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)) {
return p.getDecimalValue();
if (ctxt.isEnabled(DeserializationFeature.USE_JAVA_ARRAY_FOR_JSON_ARRAY)) {
return mapArrayToArray(p, ctxt, depth);
}
return p.getNumberValue();
return mapArray(p, ctxt, depth);
case JsonTokenId.ID_EMBEDDED_OBJECT:
return p.getEmbeddedObject();
case JsonTokenId.ID_STRING:
return p.getText();

case JsonTokenId.ID_NUMBER_INT:
if (ctxt.hasSomeOfFeatures(F_MASK_INT_COERCIONS)) {
return _coerceIntegral(p, ctxt);
}
return p.getNumberValue(); // should be optimal, whatever it is

case JsonTokenId.ID_TRUE:
return Boolean.TRUE;
case JsonTokenId.ID_FALSE:
return Boolean.FALSE;
case JsonTokenId.ID_NUMBER_FLOAT:
if (ctxt.isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)) {
return p.getDecimalValue();
}
return p.getNumberValue();

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

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

//case JsonTokenId.ID_END_ARRAY: // invalid
default:
case JsonTokenId.ID_NULL: // 08-Nov-2016, tatu: yes, occurs
return null;

//case JsonTokenId.ID_END_ARRAY: // invalid
default:
}
return ctxt.handleUnexpectedToken(Object.class, p);
}
Expand Down Expand Up @@ -858,15 +874,15 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt, Object into
return deserialize(p, ctxt);
}

protected Object mapArray(JsonParser p, DeserializationContext ctxt) throws IOException
protected Object mapArray(JsonParser p, DeserializationContext ctxt, int depth) throws IOException
{
Object value = deserialize(p, ctxt);
Object value = deserialize(p, ctxt, depth + 1);
if (p.nextToken() == JsonToken.END_ARRAY) {
ArrayList<Object> l = new ArrayList<Object>(2);
l.add(value);
return l;
}
Object value2 = deserialize(p, ctxt);
Object value2 = deserialize(p, ctxt, depth + 1);
if (p.nextToken() == JsonToken.END_ARRAY) {
ArrayList<Object> l = new ArrayList<Object>(2);
l.add(value);
Expand All @@ -880,7 +896,7 @@ protected Object mapArray(JsonParser p, DeserializationContext ctxt) throws IOEx
values[ptr++] = value2;
int totalSize = ptr;
do {
value = deserialize(p, ctxt);
value = deserialize(p, ctxt, depth + 1);
++totalSize;
if (ptr >= values.length) {
values = buffer.appendCompletedChunk(values);
Expand All @@ -897,12 +913,12 @@ protected Object mapArray(JsonParser p, DeserializationContext ctxt) throws IOEx
/**
* Method called to map a JSON Array into a Java Object array (Object[]).
*/
protected Object[] mapArrayToArray(JsonParser p, DeserializationContext ctxt) throws IOException {
protected Object[] mapArrayToArray(JsonParser p, DeserializationContext ctxt, int depth) throws IOException {
ObjectBuffer buffer = ctxt.leaseObjectBuffer();
Object[] values = buffer.resetAndStart();
int ptr = 0;
do {
Object value = deserialize(p, ctxt);
Object value = deserialize(p, ctxt, depth + 1);
if (ptr >= values.length) {
values = buffer.appendCompletedChunk(values);
ptr = 0;
Expand All @@ -915,13 +931,13 @@ protected Object[] mapArrayToArray(JsonParser p, DeserializationContext ctxt) th
/**
* Method called to map a JSON Object into a Java value.
*/
protected Object mapObject(JsonParser p, DeserializationContext ctxt) throws IOException
protected Object mapObject(JsonParser p, DeserializationContext ctxt, int depth) throws IOException
{
// will point to FIELD_NAME at this point, guaranteed
// 19-Jul-2021, tatu: Was incorrectly using "getText()" before 2.13, fixed for 2.13.0
String key1 = p.currentName();
p.nextToken();
Object value1 = deserialize(p, ctxt);
Object value1 = deserialize(p, ctxt, depth + 1);

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

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

do {
p.nextToken();
final Object newValue = deserialize(p, ctxt);
final Object newValue = deserialize(p, ctxt, depth + 1);
final Object oldValue = result.put(key, newValue);
if (oldValue != null) {
return _mapObjectWithDups(p, ctxt, result, key, oldValue, newValue,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.fasterxml.jackson.databind.deser;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.BaseMapTest;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
import java.util.Map;

public class DeepNestingUntypedDeserTest extends BaseMapTest
{
// 28-Mar-2021, tatu: Currently 3000 fails for untyped/Object,
// 4000 for untyped/Array
private final static int TOO_DEEP_NESTING = 4000;
private final static int NOT_TOO_DEEP = 1000;

private final ObjectMapper MAPPER = newJsonMapper();

public void testTooDeepUntypedWithArray() throws Exception
{
final String doc = _nestedDoc(TOO_DEEP_NESTING, "[ ", "] ");
try {
MAPPER.readValue(doc, Object.class);
fail("Should have thrown an exception.");
} catch (JsonParseException jpe) {
assertTrue(jpe.getMessage().startsWith("JSON is too deeply nested."));
}
}

public void testUntypedWithArray() throws Exception
{
final String doc = _nestedDoc(NOT_TOO_DEEP, "[ ", "] ");
Object ob = MAPPER.readValue(doc, Object.class);
assertTrue(ob instanceof List<?>);
}

public void testTooDeepUntypedWithObject() throws Exception
{
final String doc = "{"+_nestedDoc(TOO_DEEP_NESTING, "\"x\":{", "} ") + "}";
try {
MAPPER.readValue(doc, Object.class);
fail("Should have thrown an exception.");
} catch (JsonParseException jpe) {
assertTrue(jpe.getMessage().startsWith("JSON is too deeply nested."));
}
}

public void testUntypedWithObject() throws Exception
{
final String doc = "{"+_nestedDoc(NOT_TOO_DEEP, "\"x\":{", "} ") + "}";
Object ob = MAPPER.readValue(doc, Object.class);
assertTrue(ob instanceof Map<?, ?>);
}

private String _nestedDoc(int nesting, String open, String close) {
StringBuilder sb = new StringBuilder(nesting * (open.length() + close.length()));
for (int i = 0; i < nesting; ++i) {
sb.append(open);
if ((i & 31) == 0) {
sb.append("\n");
}
}
for (int i = 0; i < nesting; ++i) {
sb.append(close);
if ((i & 31) == 0) {
sb.append("\n");
}
}
return sb.toString();
}
}

1 comment on commit fcfc499

@AshishKmrSingh
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was raised by the black duck to be DoS attack vulnerable. The changes done here in are published as a snapshot version which the Black duck is showing as non-vulnerable now. But, I am not able to figure out how to use that version in my build.gradle. I cannot find the version on nexus- not in snapshot and release repositories.
Please help.

Please sign in to comment.