Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Throw JsonMappingException for deeply nested JSON (#2816, CVE-2020-36518) #3416

Merged
merged 3 commits into from
Mar 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Member

Choose a reason for hiding this comment

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

@cowtowncoder @TaylorSMarks would it make sense to add a public static method to UntypedObjectDeserializer.java that allows users to set a different limit (but still with 1000 default)?


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:
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();
}
}