VYPR
High severityNVD Advisory· Published Mar 11, 2022· Updated Aug 27, 2025

CVE-2020-36518

CVE-2020-36518

Description

jackson-databind before 2.13.0 allows a Java StackOverflow exception and denial of service via a large depth of nested objects.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

A stack overflow in jackson-databind's UntypedObjectDeserializer allows denial of service via deeply nested JSON objects.

Vulnerability

CVE-2020-36518 is a denial-of-service vulnerability in FasterXML jackson-databind affecting versions before 2.13.0. The flaw resides in the UntypedObjectDeserializer (Vanilla variant), which recursively deserializes nested JSON structures without a depth limit. When processing a JSON payload with an excessive nesting depth, the recursive calls cause a StackOverflowError, crashing the application thread. The fix, introduced in commit fcfc4998 [4], adds a MAX_DEPTH constant of 1000 and throws a JsonParseException when the depth is exceeded. Micro-patches addressing this issue were released for the 2.12.x and 2.13.x branches: jackson-databind 2.12.6.1 [3] and jackson-databind 2.13.2.1 [2].

Exploitation

An attacker can exploit this vulnerability by sending a crafted JSON payload with deeply nested objects (e.g., thousands of levels) to any service that uses jackson-databind to parse untrusted JSON input. No authentication or special privileges are required if the service accepts JSON from external sources. The parser will recursively call deserialize() on each nested level until the Java call stack overflows, resulting in a StackOverflowError.

Impact

Successful exploitation leads to a denial of service (DoS). The Java process throws a StackOverflowError, which may terminate the current thread or, depending on the application's error handling, crash the entire JVM. There is no impact on confidentiality or integrity; the vulnerability only affects availability.

Mitigation

The vulnerability is fixed in jackson-databind version 2.13.0 (released September 30, 2021) and in subsequent micro-patches 2.13.2.1 [2] and 2.12.6.1 [3]. Users should upgrade to at least these versions. If an immediate upgrade is not possible, a workaround is to implement a custom deserializer that enforces a depth limit, or to use an alternative JSON parser that provides built-in depth control. No other mitigations are documented in the available references.

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
com.fasterxml.jackson.core:jackson-databindMaven
>= 2.13.0, < 2.13.2.12.13.2.1
com.fasterxml.jackson.core:jackson-databindMaven
< 2.12.6.12.12.6.1

Affected products

109

Patches

7
f758e50f0ecb

[maven-release-plugin] prepare release jackson-databind-2.12.6.1

https://github.com/FasterXML/jackson-databindTatu SalorantaMar 27, 2022via osv
1 file changed · +2 2
  • pom.xml+2 2 modified
    @@ -15,7 +15,7 @@
     
       <groupId>com.fasterxml.jackson.core</groupId>
       <artifactId>jackson-databind</artifactId>
    -  <version>2.12.6.1-SNAPSHOT</version>
    +  <version>2.12.6.1</version>
       <name>jackson-databind</name>
       <packaging>bundle</packaging>
       <description>General data-binding functionality for Jackson: works on core streaming API</description>
    @@ -33,7 +33,7 @@
         <connection>scm:git:git@github.com:FasterXML/jackson-databind.git</connection>
         <developerConnection>scm:git:git@github.com:FasterXML/jackson-databind.git</developerConnection>
         <url>http://github.com/FasterXML/jackson-databind</url>
    -    <tag>HEAD</tag>
    +    <tag>jackson-databind-2.12.6.1</tag>
       </scm>
     
       <properties>
    
bdf6441dac25

[maven-release-plugin] prepare release jackson-databind-2.13.2.1

https://github.com/FasterXML/jackson-databindTatu SalorantaMar 25, 2022via osv
1 file changed · +2 2
  • pom.xml+2 2 modified
    @@ -13,7 +13,7 @@
       </parent>
       <groupId>com.fasterxml.jackson.core</groupId>
       <artifactId>jackson-databind</artifactId>
    -  <version>2.13.2.1-SNAPSHOT</version>
    +  <version>2.13.2.1</version>
       <name>jackson-databind</name>
       <packaging>bundle</packaging>
       <description>General data-binding functionality for Jackson: works on core streaming API</description>
    @@ -31,7 +31,7 @@
         <connection>scm:git:git@github.com:FasterXML/jackson-databind.git</connection>
         <developerConnection>scm:git:git@github.com:FasterXML/jackson-databind.git</developerConnection>
         <url>http://github.com/FasterXML/jackson-databind</url>
    -    <tag>HEAD</tag>
    +    <tag>jackson-databind-2.13.2.1</tag>
       </scm>
     
       <properties>
    
8238ab41d035

Fix #3473 (re-implementation of #2816 for 2.14)

https://github.com/FasterXML/jackson-databindTatu SalorantaMay 31, 2022via ghsa
6 files changed · +666 71
  • src/main/java/com/fasterxml/jackson/databind/deser/std/UntypedObjectDeserializer.java+4 35 modified
    @@ -196,7 +196,7 @@ public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
             if ((_stringDeserializer == null) && (_numberDeserializer == null)
                     && (_mapDeserializer == null) && (_listDeserializer == null)
                     &&  getClass() == UntypedObjectDeserializer.class) {
    -            return Vanilla.instance(preventMerge);
    +            return UntypedObjectDeserializerNR.instance(preventMerge);
             }
     
             if (preventMerge != _nonMerging) {
    @@ -659,6 +659,7 @@ protected Object mapObject(JsonParser p, DeserializationContext ctxt,
          * is only used when no custom deserializer overrides are applied.
          */
         @JacksonStdImpl
    +    @Deprecated // since 2.14, to be removed in near future
         public static class Vanilla
             extends StdDeserializer<Object>
         {
    @@ -869,18 +870,10 @@ protected Object mapArray(JsonParser p, DeserializationContext ctxt) throws IOEx
                     l.add(value);
                     return l;
                 }
    -            Object value2 = deserialize(p, ctxt);
    -            if (p.nextToken()  == JsonToken.END_ARRAY) {
    -                ArrayList<Object> l = new ArrayList<Object>(2);
    -                l.add(value);
    -                l.add(value2);
    -                return l;
    -            }
                 ObjectBuffer buffer = ctxt.leaseObjectBuffer();
                 Object[] values = buffer.resetAndStart();
                 int ptr = 0;
                 values[ptr++] = value;
    -            values[ptr++] = value2;
                 int totalSize = ptr;
                 do {
                     value = deserialize(p, ctxt);
    @@ -898,9 +891,6 @@ protected Object mapArray(JsonParser p, DeserializationContext ctxt) throws IOEx
                 return result;
             }
     
    -        /**
    -         * Method called to map a JSON Array into a Java Object array (Object[]).
    -         */
             protected Object[] mapArrayToArray(JsonParser p, DeserializationContext ctxt) throws IOException {
                 ObjectBuffer buffer = ctxt.leaseObjectBuffer();
                 Object[] values = buffer.resetAndStart();
    @@ -918,9 +908,6 @@ protected Object[] mapArrayToArray(JsonParser p, DeserializationContext ctxt) th
                 return result;
             }
     
    -        /**
    -         * Method called to map a JSON Object into a Java value.
    -         */
             protected Object mapObject(JsonParser p, DeserializationContext ctxt) throws IOException
             {
                 // will point to FIELD_NAME at this point, guaranteed
    @@ -929,33 +916,15 @@ protected Object mapObject(JsonParser p, DeserializationContext ctxt) throws IOE
                 p.nextToken();
                 Object value1 = deserialize(p, ctxt);
     
    -            String key2 = p.nextFieldName();
    -            if (key2 == null) { // single entry; but we want modifiable
    -                LinkedHashMap<String, Object> result = new LinkedHashMap<String, Object>(2);
    -                result.put(key1, value1);
    -                return result;
    -            }
    -            p.nextToken();
    -            Object value2 = deserialize(p, ctxt);
    -
                 String key = p.nextFieldName();
    -            if (key == null) {
    -                LinkedHashMap<String, Object> result = new LinkedHashMap<String, Object>(4);
    +            if (key == null) { // single entry; but we want modifiable
    +                LinkedHashMap<String, Object> result = new LinkedHashMap<String, Object>(2);
                     result.put(key1, value1);
    -                if (result.put(key2, value2) != null) {
    -                    // 22-May-2020, tatu: [databind#2733] may need extra handling
    -                    return _mapObjectWithDups(p, ctxt, result, key1, value1, value2, key);
    -                }
                     return result;
                 }
                 // And then the general case; default map size is 16
                 LinkedHashMap<String, Object> result = new LinkedHashMap<String, Object>();
                 result.put(key1, value1);
    -            if (result.put(key2, value2) != null) {
    -                // 22-May-2020, tatu: [databind#2733] may need extra handling
    -                return _mapObjectWithDups(p, ctxt, result, key1, value1, value2, key);
    -            }
    -
                 do {
                     p.nextToken();
                     final Object newValue = deserialize(p, ctxt);
    
  • src/main/java/com/fasterxml/jackson/databind/deser/std/UntypedObjectDeserializerNR.java+640 0 added
    @@ -0,0 +1,640 @@
    +package com.fasterxml.jackson.databind.deser.std;
    +
    +import java.io.IOException;
    +import java.util.ArrayList;
    +import java.util.Collection;
    +import java.util.LinkedHashMap;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.Objects;
    +
    +import com.fasterxml.jackson.core.JsonParser;
    +import com.fasterxml.jackson.core.JsonToken;
    +import com.fasterxml.jackson.core.JsonTokenId;
    +import com.fasterxml.jackson.core.StreamReadCapability;
    +
    +import com.fasterxml.jackson.databind.DeserializationConfig;
    +import com.fasterxml.jackson.databind.DeserializationContext;
    +import com.fasterxml.jackson.databind.DeserializationFeature;
    +import com.fasterxml.jackson.databind.annotation.JacksonStdImpl;
    +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
    +import com.fasterxml.jackson.databind.type.LogicalType;
    +
    +/**
    + * @since 2.14
    + */
    +@JacksonStdImpl
    +final class UntypedObjectDeserializerNR
    +    extends StdDeserializer<Object>
    +{
    +    private static final long serialVersionUID = 1L;
    +
    +    protected final static Object[] NO_OBJECTS = new Object[0];
    +
    +    public final static UntypedObjectDeserializerNR std = new UntypedObjectDeserializerNR();
    +
    +    // @since 2.9
    +    protected final boolean _nonMerging;
    +    
    +    public UntypedObjectDeserializerNR() { this(false); }
    +
    +    protected UntypedObjectDeserializerNR(boolean nonMerging) {
    +        super(Object.class);
    +        _nonMerging = nonMerging;
    +    }
    +
    +    public static UntypedObjectDeserializerNR instance(boolean nonMerging) {
    +        if (nonMerging) {
    +            return new UntypedObjectDeserializerNR(true);
    +        }
    +        return std;
    +    }
    +
    +    @Override
    +    public LogicalType logicalType() {
    +        return LogicalType.Untyped;
    +    }
    +
    +    @Override // since 2.9
    +    public Boolean supportsUpdate(DeserializationConfig config) {
    +        // 21-Apr-2017, tatu: Bit tricky... some values, yes. So let's say "dunno"
    +        // 14-Jun-2017, tatu: Well, if merging blocked, can say no, as well.
    +        return _nonMerging ? Boolean.FALSE : null;
    +    }
    +
    +    @Override
    +    public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException
    +    {
    +        switch (p.currentTokenId()) {
    +        case JsonTokenId.ID_START_OBJECT:
    +            return _deserializeNR(p, ctxt,
    +                    Scope.rootObjectScope(ctxt.isEnabled(StreamReadCapability.DUPLICATE_PROPERTIES)));
    +        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 Scope.emptyMap();
    +        case JsonTokenId.ID_FIELD_NAME:
    +            return _deserializeObjectAtName(p, ctxt);
    +        case JsonTokenId.ID_START_ARRAY:
    +            return _deserializeNR(p, ctxt, Scope.rootArrayScope());
    +
    +        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();
    +        case JsonTokenId.ID_NUMBER_FLOAT:
    +            if (ctxt.isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)) {
    +                return p.getDecimalValue();
    +            }
    +            return p.getNumberValue();
    +        case JsonTokenId.ID_TRUE:
    +            return Boolean.TRUE;
    +        case JsonTokenId.ID_FALSE:
    +            return Boolean.FALSE;
    +        case JsonTokenId.ID_NULL:
    +            return null;
    +        case JsonTokenId.ID_EMBEDDED_OBJECT:
    +            return p.getEmbeddedObject();
    +        default:
    +        }
    +        return ctxt.handleUnexpectedToken(getValueType(ctxt), p);
    +    }
    +
    +    @Override
    +    public Object deserializeWithType(JsonParser p, DeserializationContext ctxt,
    +            TypeDeserializer typeDeserializer)
    +        throws IOException
    +    {
    +        switch (p.currentTokenId()) {
    +        case JsonTokenId.ID_START_ARRAY:
    +        case JsonTokenId.ID_START_OBJECT:
    +        case JsonTokenId.ID_FIELD_NAME:
    +            return typeDeserializer.deserializeTypedFromAny(p, ctxt);
    +        default:
    +            return _deserializeAnyScalar(p, ctxt, p.currentTokenId());
    +        }
    +    }
    +
    +    @SuppressWarnings("unchecked")
    +    @Override
    +    public Object deserialize(JsonParser p, DeserializationContext ctxt, Object intoValue)
    +        throws IOException
    +    {
    +        if (_nonMerging) {
    +            return deserialize(p, ctxt);
    +        }
    +        switch (p.currentTokenId()) {
    +        case JsonTokenId.ID_END_OBJECT:
    +        case JsonTokenId.ID_END_ARRAY:
    +            return intoValue;
    +        case JsonTokenId.ID_START_OBJECT:
    +            {
    +                JsonToken t = p.nextToken(); // to get to FIELD_NAME or END_OBJECT
    +                if (t == JsonToken.END_OBJECT) {
    +                    return intoValue;
    +                }
    +            }
    +            // fall through
    +        case JsonTokenId.ID_FIELD_NAME:
    +            if (intoValue instanceof Map<?,?>) {
    +                Map<Object,Object> m = (Map<Object,Object>) intoValue;
    +                // NOTE: we are guaranteed to point to FIELD_NAME
    +                String key = p.currentName();
    +                do {
    +                    p.nextToken();
    +                    // and possibly recursive merge here
    +                    Object old = m.get(key);
    +                    Object newV;
    +                    if (old != null) {
    +                        newV = deserialize(p, ctxt, old);
    +                    } else {
    +                        newV = deserialize(p, ctxt);
    +                    }
    +                    if (newV != old) {
    +                        m.put(key, newV);
    +                    }
    +                } while ((key = p.nextFieldName()) != null);
    +                return intoValue;
    +            }
    +            break;
    +        case JsonTokenId.ID_START_ARRAY:
    +            {
    +                JsonToken t = p.nextToken(); // to get to FIELD_NAME or END_OBJECT
    +                if (t == JsonToken.END_ARRAY) {
    +                    return intoValue;
    +                }
    +            }
    +
    +            if (intoValue instanceof Collection<?>) {
    +                Collection<Object> c = (Collection<Object>) intoValue;
    +                // NOTE: merge for arrays/Collections means append, can't merge contents
    +                do {
    +                    c.add(deserialize(p, ctxt));
    +                } while (p.nextToken() != JsonToken.END_ARRAY);
    +                return intoValue;
    +            }
    +            // 21-Apr-2017, tatu: Should we try to support merging of Object[] values too?
    +            //    ... maybe future improvement
    +            break;
    +        }
    +        // Easiest handling for the rest, delegate. Only (?) question: how about nulls?
    +        return deserialize(p, ctxt);
    +    }
    +
    +    private Object _deserializeObjectAtName(JsonParser p, DeserializationContext ctxt)
    +        throws IOException
    +    {
    +        final Scope rootObject = Scope.rootObjectScope(ctxt.isEnabled(StreamReadCapability.DUPLICATE_PROPERTIES));
    +        String key = p.currentName();
    +        for (; key != null; key = p.nextFieldName()) {
    +            Object value;
    +            JsonToken t = p.nextToken();
    +            if (t == null) { // can this ever occur?
    +                t = JsonToken.NOT_AVAILABLE;
    +            }
    +            switch (t.id()) {
    +            case JsonTokenId.ID_START_OBJECT:
    +                value = _deserializeNR(p, ctxt, rootObject.childObject());
    +                break;
    +            case JsonTokenId.ID_END_OBJECT:
    +                return rootObject.finishRootObject();
    +            case JsonTokenId.ID_START_ARRAY:
    +                value = _deserializeNR(p, ctxt, rootObject.childArray());
    +                break;
    +            default:
    +                value = _deserializeAnyScalar(p, ctxt, t.id());
    +            }
    +            rootObject.putValue(key, value);
    +        }
    +        return rootObject.finishRootObject();
    +    }
    +
    +    private Object _deserializeNR(JsonParser p, DeserializationContext ctxt,
    +            Scope rootScope)
    +        throws IOException
    +    {
    +        final boolean intCoercions = ctxt.hasSomeOfFeatures(F_MASK_INT_COERCIONS);
    +        final boolean useJavaArray = ctxt.isEnabled(DeserializationFeature.USE_JAVA_ARRAY_FOR_JSON_ARRAY);
    +
    +        Scope currScope = rootScope;
    +
    +        outer_loop:
    +        while (true) {
    +            if (currScope.isObject()) {
    +                String propName = p.nextFieldName();
    +
    +                objectLoop:
    +                for (; propName != null; propName = p.nextFieldName()) {
    +                    Object value;
    +                    JsonToken t = p.nextToken();
    +                    if (t == null) { // unexpected end-of-input (or bad buffering?)
    +                        t = JsonToken.NOT_AVAILABLE; // to trigger an exception
    +                    }
    +                    switch (t.id()) {
    +                    case JsonTokenId.ID_START_OBJECT:
    +                        currScope = currScope.childObject(propName);
    +                        // We can actually take a short-cut with nested Objects...
    +                        continue objectLoop;
    +                    case JsonTokenId.ID_START_ARRAY:
    +                        currScope = currScope.childArray(propName);
    +                        // but for arrays need to go to main loop
    +                        continue outer_loop;
    +                    case JsonTokenId.ID_STRING:
    +                        value = p.getText();
    +                        break;
    +                    case JsonTokenId.ID_NUMBER_INT:
    +                        value = intCoercions ?  _coerceIntegral(p, ctxt) : p.getNumberValue();
    +                        break;
    +                    case JsonTokenId.ID_NUMBER_FLOAT:
    +                        value = ctxt.isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)
    +                            ? p.getDecimalValue() : p.getNumberValue();
    +                        break;
    +                    case JsonTokenId.ID_TRUE:
    +                        value = Boolean.TRUE;
    +                        break;
    +                    case JsonTokenId.ID_FALSE:
    +                        value = Boolean.FALSE;
    +                        break;
    +                    case JsonTokenId.ID_NULL:
    +                        value = null;
    +                        break;
    +                    case JsonTokenId.ID_EMBEDDED_OBJECT:
    +                        value = p.getEmbeddedObject();
    +                        break;
    +                    default:
    +                        return ctxt.handleUnexpectedToken(getValueType(ctxt), p);
    +                    }
    +                    currScope.putValue(propName, value);
    +                }
    +                // reached not-property-name, should be END_OBJECT (verify?)
    +                if (currScope == rootScope) {
    +                    return currScope.finishRootObject();
    +                }
    +                currScope = currScope.finishBranchObject();
    +            } else {
    +                // Otherwise we must have an Array
    +                arrayLoop:
    +                while (true) {
    +                    JsonToken t = p.nextToken();
    +                    if (t == null) { // unexpected end-of-input (or bad buffering?)
    +                        t = JsonToken.NOT_AVAILABLE; // to trigger an exception
    +                    }
    +                    Object value;
    +                    switch (t.id()) {
    +                    case JsonTokenId.ID_START_OBJECT:
    +                        currScope = currScope.childObject();
    +                        continue outer_loop;
    +                    case JsonTokenId.ID_START_ARRAY:
    +                        currScope = currScope.childArray();
    +                        continue outer_loop;
    +                    case JsonTokenId.ID_END_ARRAY:
    +                        if (currScope == rootScope) {
    +                            return currScope.finishRootArray(useJavaArray);
    +                        }
    +                        currScope = currScope.finishBranchArray(useJavaArray);
    +                        break arrayLoop;
    +                    case JsonTokenId.ID_STRING:
    +                        value = p.getText();
    +                        break;
    +                    case JsonTokenId.ID_NUMBER_INT:
    +                        value = intCoercions ?  _coerceIntegral(p, ctxt) : p.getNumberValue();
    +                        break;
    +                    case JsonTokenId.ID_NUMBER_FLOAT:
    +                        value = ctxt.isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)
    +                            ? p.getDecimalValue() : p.getNumberValue();
    +                        break;
    +                    case JsonTokenId.ID_TRUE:
    +                        value = Boolean.TRUE;
    +                        break;
    +                    case JsonTokenId.ID_FALSE:
    +                        value = Boolean.FALSE;
    +                        break;
    +                    case JsonTokenId.ID_NULL:
    +                        value = null;
    +                        break;
    +                    case JsonTokenId.ID_EMBEDDED_OBJECT:
    +                        value = p.getEmbeddedObject();
    +                        break;
    +                    default:
    +                        return ctxt.handleUnexpectedToken(getValueType(ctxt), p);
    +                    }
    +                    currScope.addValue(value);
    +                }
    +            }
    +        }
    +    }
    +
    +    private Object _deserializeAnyScalar(JsonParser p, DeserializationContext ctxt,
    +            int tokenType) 
    +        throws IOException
    +    {
    +        switch (tokenType) {
    +        case JsonTokenId.ID_STRING:
    +            return p.getText();
    +        case JsonTokenId.ID_NUMBER_INT:
    +            if (ctxt.isEnabled(DeserializationFeature.USE_BIG_INTEGER_FOR_INTS)) {
    +                return p.getBigIntegerValue();
    +            }
    +            return p.getNumberValue();
    +
    +        case JsonTokenId.ID_NUMBER_FLOAT:
    +            if (ctxt.isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)) {
    +                return p.getDecimalValue();
    +            }
    +            return p.getNumberValue();
    +        case JsonTokenId.ID_TRUE:
    +            return Boolean.TRUE;
    +        case JsonTokenId.ID_FALSE:
    +            return Boolean.FALSE;
    +        case JsonTokenId.ID_EMBEDDED_OBJECT:
    +            return p.getEmbeddedObject();
    +        
    +        case JsonTokenId.ID_NULL: // should not get this far really but...
    +            return null;
    +        // Caller should check for anything else
    +        default:
    +        }
    +        return ctxt.handleUnexpectedToken(getValueType(ctxt), p);
    +    }
    +
    +    // NOTE: copied from above (alas, no easy way to share/reuse)
    +    // @since 2.12 (wrt [databind#2733]
    +    protected Object _mapObjectWithDups(JsonParser p, DeserializationContext ctxt,
    +            final Map<String, Object> result, String initialKey,
    +            Object oldValue, Object newValue, String nextKey) throws IOException
    +    {
    +        final boolean squashDups = ctxt.isEnabled(StreamReadCapability.DUPLICATE_PROPERTIES);
    +
    +        if (squashDups) {
    +            _squashDups(result, initialKey, oldValue, newValue);
    +        }
    +
    +        while (nextKey != null) {
    +            p.nextToken();
    +            newValue = deserialize(p, ctxt);
    +            oldValue = result.put(nextKey, newValue);
    +            if ((oldValue != null) && squashDups) {
    +                _squashDups(result, nextKey, oldValue, newValue);
    +            }
    +            nextKey = p.nextFieldName();
    +        }
    +
    +        return result;
    +    }
    +
    +    // NOTE: copied from above (alas, no easy way to share/reuse)
    +    @SuppressWarnings("unchecked")
    +    private void _squashDups(final Map<String, Object> result, String key,
    +            Object oldValue, Object newValue)
    +    {
    +        if (oldValue instanceof List<?>) {
    +            ((List<Object>) oldValue).add(newValue);
    +            result.put(key, oldValue);
    +        } else {
    +            ArrayList<Object> l = new ArrayList<>();
    +            l.add(oldValue);
    +            l.add(newValue);
    +            result.put(key, l);
    +        }
    +    }    
    +
    +    /*
    +    /**********************************************************************
    +    /* Helper classes
    +    /**********************************************************************
    +     */
    +
    +    /**
    +     * Helper class used for building Maps and Lists/Arrays.
    +     */
    +    private final static class Scope
    +    {
    +        private final Scope _parent;
    +        private Scope _child;
    +
    +        private boolean _isObject, _squashDups;
    +        private String  _deferredKey;
    +
    +        private Map<String, Object> _map;
    +        private List<Object> _list;
    +
    +        /*
    +        /******************************************************************
    +        /* Life cycle
    +        /******************************************************************
    +         */
    +
    +        // For Arrays:
    +        private Scope(Scope p) {
    +            _parent = p;
    +            _isObject = false;
    +            _squashDups = false;
    +        }
    +
    +        // For Objects:
    +        private Scope(Scope p, boolean isObject, boolean squashDups) {
    +            _parent = p;
    +            _isObject = isObject;
    +            _squashDups = squashDups;
    +        }
    +
    +        public static Scope rootObjectScope(boolean squashDups) {
    +            return new Scope(null, true, squashDups);
    +        }
    +
    +        public static Scope rootArrayScope() {
    +            return new Scope(null);
    +        }
    +
    +        private Scope resetAsArray() {
    +            _isObject = false;
    +            return this;
    +        }
    +
    +        private Scope resetAsObject(boolean squashDups) {
    +            _isObject = true;
    +            _squashDups = squashDups;
    +            return this;
    +        }
    +
    +        public Scope childObject() {
    +            if (_child == null) {
    +                return new Scope(this, true, _squashDups);
    +            }
    +            return _child.resetAsObject(_squashDups);
    +        }
    +
    +        public Scope childObject(String deferredKey) {
    +            _deferredKey = deferredKey;
    +            if (_child == null) {
    +                return new Scope(this, true, _squashDups);
    +            }
    +            return _child.resetAsObject(_squashDups);
    +        }
    +
    +        public Scope childArray() {
    +            if (_child == null) {
    +                return new Scope(this);
    +            }
    +            return _child.resetAsArray();
    +        }
    +
    +        public Scope childArray(String deferredKey) {
    +            _deferredKey = deferredKey;
    +            if (_child == null) {
    +                return new Scope(this);
    +            }
    +            return _child.resetAsArray();
    +        }
    +
    +        /*
    +        /******************************************************************
    +        /* Accessors
    +        /******************************************************************
    +         */
    +
    +        public boolean isObject() {
    +            return _isObject;
    +        }
    +
    +        /*
    +        /******************************************************************
    +        /* Value construction
    +        /******************************************************************
    +         */
    +
    +        public void putValue(String key, Object value) {
    +            if (_squashDups) {
    +                _putValueHandleDups(key, value);
    +                return;
    +            }
    +            if (_map == null) {
    +                _map = new LinkedHashMap<>();
    +            }
    +            _map.put(key, value);
    +        }
    +
    +        public Scope putDeferredValue(Object value) {
    +            String key = Objects.requireNonNull(_deferredKey);
    +            _deferredKey = null;
    +            if (_squashDups) {
    +                _putValueHandleDups(key, value);
    +                return this;
    +            }
    +            if (_map == null) {
    +                _map = new LinkedHashMap<>();
    +            }
    +            _map.put(key, value);
    +            return this;
    +        }
    +
    +        public void addValue(Object value) {
    +            if (_list == null) {
    +                _list = new ArrayList<>();
    +            }
    +            _list.add(value);
    +        }
    +
    +        public Object finishRootObject() {
    +            if (_map == null)  {
    +                return emptyMap();
    +            }
    +            return _map;
    +        }
    +
    +        public Scope finishBranchObject() {
    +            Object value;
    +            if (_map == null) {
    +                value = new LinkedHashMap<>();
    +            } else {
    +                value = _map;
    +                _map = null;
    +            }
    +            if (_parent.isObject()) {
    +                return _parent.putDeferredValue(value);
    +            }
    +            _parent.addValue(value);
    +            return _parent;
    +        }
    +
    +        public Object finishRootArray(boolean asJavaArray) {
    +            if (_list == null) {
    +                if (asJavaArray) {
    +                    return NO_OBJECTS;
    +                }
    +                return emptyList();
    +            }
    +            if (asJavaArray) {
    +                return _list.toArray(NO_OBJECTS);
    +            }
    +            return _list;
    +        }
    +
    +        public Scope finishBranchArray(boolean asJavaArray) {
    +            Object value;
    +            if (_list == null) {
    +                if (asJavaArray) {
    +                    value = NO_OBJECTS;
    +                } else {
    +                    value = emptyList();
    +                }
    +            } else {
    +                if (asJavaArray) {
    +                    value = _list.toArray(NO_OBJECTS);
    +                } else {
    +                    value = _list;
    +                }
    +                _list = null;
    +            }
    +            if (_parent.isObject()) {
    +                return _parent.putDeferredValue(value);
    +            }
    +            _parent.addValue(value);
    +            return _parent;
    +        }
    +
    +        /* Helper method that deals with merging of dups, when that is expected.
    +         * Only used with formats that expose seeming "duplicates" in Object
    +         * values: most notable this is the case for XML.
    +         */
    +        @SuppressWarnings("unchecked")
    +        private void _putValueHandleDups(String key, Object newValue) {
    +            if (_map == null) {
    +                _map = new LinkedHashMap<>();
    +                _map.put(key, newValue);
    +                return;
    +            }
    +            Object old = _map.put(key, newValue);
    +            if (old != null) {
    +                // If value was already a List, append
    +                if (old instanceof List<?>) {
    +                    ((List<Object>) old).add(newValue);
    +                    _map.put(key, old);
    +                } else { // but if not (Object or, possible, Java array), make it such
    +                    ArrayList<Object> l = new ArrayList<>();
    +                    l.add(old);
    +                    l.add(newValue);
    +                    _map.put(key, l);
    +                }
    +                
    +            }
    +        }
    +
    +        /*
    +        /******************************************************************
    +        /* Helper methods
    +        /******************************************************************
    +         */
    +
    +        public static Map<String, Object> emptyMap() {
    +            return new LinkedHashMap<>(2);
    +        }
    +
    +        public static List<Object> emptyList() {
    +            return new ArrayList<>(2);
    +        }
    +    }
    +}
    
  • src/test/java/com/fasterxml/jackson/databind/deser/dos/DeepJsonNodeDeser3397Test.java+2 2 modified
    @@ -8,9 +8,9 @@ public class DeepJsonNodeDeser3397Test extends BaseMapTest
         // 28-Mar-2021, tatu: Used to fail at 5000 for tree/object,
         // 8000 for tree/array, before work on iterative JsonNode deserializer
         // ... currently gets a bit slow at 1M but passes.
    -    // But test with 50k as practical limit, to guard against regression
    +    // But test with 100k as practical limit, to guard against regression
     //    private final static int TOO_DEEP_NESTING = 1_000_000;
    -    private final static int TOO_DEEP_NESTING = 49999;
    +    private final static int TOO_DEEP_NESTING = 100_00;
     
         private final ObjectMapper MAPPER = newJsonMapper();
     
    
  • src/test/java/com/fasterxml/jackson/databind/deser/dos/DeepNestingUntypedDeserTest.java+16 31 modified
    @@ -4,55 +4,40 @@
     import java.util.Map;
     
     import com.fasterxml.jackson.databind.BaseMapTest;
    +import com.fasterxml.jackson.databind.DeserializationFeature;
     import com.fasterxml.jackson.databind.ObjectMapper;
     
    -// For [databind#2816]
    +// For [databind#2816] / [databind#3473]
     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;
    +    // 31-May-2022, tatu: But no more! Can handle much much larger
    +    //   nesting levels, bounded by memory usage not stack. Tested with
    +    //   1 million (!) nesting levels, but to keep tests fast use 100k
    +    private final static int TOO_DEEP_NESTING = 100_000;
     
         private final ObjectMapper MAPPER = newJsonMapper();
     
    -    public void testUntypedWithArray() throws Exception
    +    public void testFormerlyTooDeepUntypedWithArray() throws Exception
         {
    -        final String doc = _nestedDoc(NOT_TOO_DEEP, "[ ", "] ");
    +        final String doc = _nestedDoc(TOO_DEEP_NESTING, "[ ", "] ");
             Object ob = MAPPER.readValue(doc, Object.class);
             assertTrue(ob instanceof List<?>);
    -    }
     
    -    public void testUntypedWithObject() throws Exception
    -    {
    -        final String doc = "{"+_nestedDoc(NOT_TOO_DEEP, "\"x\":{", "} ") + "}";
    -        Object ob = MAPPER.readValue(doc, Object.class);
    -        assertTrue(ob instanceof Map<?, ?>);
    +        // ... but also work with Java array
    +        ob = MAPPER.readerFor(Object.class)
    +                .with(DeserializationFeature.USE_JAVA_ARRAY_FOR_JSON_ARRAY)
    +                .readValue(doc, Object.class);
    +        assertTrue(ob instanceof Object[]);
         }
     
    -    /*// Until #2816 equivalent implemented for 2.14
    -    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 (StreamReadException e) {
    -            verifyException(e, "JSON is too deeply nested.");
    -        }
    -    }
    -
    -    public void testTooDeepUntypedWithObject() throws Exception
    +    public void testFormerlyTooDeepUntypedWithObject() throws Exception
         {
             final String doc = "{"+_nestedDoc(TOO_DEEP_NESTING, "\"x\":{", "} ") + "}";
    -        try {
    -            MAPPER.readValue(doc, Object.class);
    -            fail("Should have thrown an exception.");
    -        } catch (StreamReadException e) {
    -            verifyException(e, "JSON is too deeply nested.");
    -        }
    +        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()));
    
  • src/test/java/com/fasterxml/jackson/databind/deser/jdk/MapDeserializationTest.java+1 1 modified
    @@ -90,7 +90,7 @@ static class AbstractMapWrapper {
         /**********************************************************
          */
     
    -    private final ObjectMapper MAPPER = new ObjectMapper();
    +    private final ObjectMapper MAPPER = newJsonMapper();
     
         public void testBigUntypedMap() throws Exception
         {
    
  • src/test/java/com/fasterxml/jackson/databind/ser/TestTreeSerialization.java+3 2 modified
    @@ -37,14 +37,15 @@ public void testSimpleViaObjectMapper()
             mapper.writeTree(jg, n);
     
             Map<String,Object> result = (Map<String,Object>) mapper.readValue(sw.toString(), Map.class);
    -
             assertEquals(3, result.size());
             assertEquals("abc", result.get("string"));
             assertEquals(Integer.valueOf(15), result.get("number"));
             Map<String,Object> ob = (Map<String,Object>) result.get("ob");
             assertEquals(1, ob.size());
             List<Object> list = (List<Object>) ob.get("arr");
    -        assertNotNull(list);
    +        if (list == null) {
    +            fail("Missing entry 'arr': "+ob);
    +        }
             assertEquals(0, list.size());
             jg.close();
         }
    
3cc52f82ecf9

Add release notes, minor clean up for #2816 fix.

https://github.com/FasterXML/jackson-databindTatu SalorantaMar 22, 2022via ghsa
9 files changed · +98 86
  • release-notes/CREDITS-2.x+8 0 modified
    @@ -1428,3 +1428,11 @@ Matthieu Finiasz (finiasz@github)
       * Reported #3412: Version 2.13.2 uses `Method.getParameterCount()` which is not
        supported on Android before API 26
       (2.13.2)
    +
    +Taylor S Marks (TaylorSMarks@github)
    +  * Contributed fix for #2816: Optimize UntypedObjectDeserializer wrt recursion
    +  (2.13.3)
    +
    +Spence Nace (???@github)
    +  * Contributed fix for #2816: Optimize UntypedObjectDeserializer wrt recursion
    +  (2.13.3)
    
  • release-notes/VERSION-2.x+2 0 modified
    @@ -6,6 +6,8 @@ Project: jackson-databind
     
     2.13.3 (not yet released)
     
    +#2816: Optimize UntypedObjectDeserializer wrt recursion
    + (contributed by Taylor S, Spence N)
     #3412: Version 2.13.2 uses `Method.getParameterCount()` which is not
       supported on Android before API 26
      (reported by Matthew F)
    
  • src/main/java/com/fasterxml/jackson/databind/deser/std/UntypedObjectDeserializer.java+10 7 modified
    @@ -876,13 +876,14 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt, Object into
     
             protected Object mapArray(JsonParser p, DeserializationContext ctxt, int depth) throws IOException
             {
    -            Object value = deserialize(p, ctxt, depth + 1);
    +            ++depth;
    +            Object value = deserialize(p, ctxt, depth);
                 if (p.nextToken()  == JsonToken.END_ARRAY) {
                     ArrayList<Object> l = new ArrayList<Object>(2);
                     l.add(value);
                     return l;
                 }
    -            Object value2 = deserialize(p, ctxt, depth + 1);
    +            Object value2 = deserialize(p, ctxt, depth);
                 if (p.nextToken()  == JsonToken.END_ARRAY) {
                     ArrayList<Object> l = new ArrayList<Object>(2);
                     l.add(value);
    @@ -896,7 +897,7 @@ protected Object mapArray(JsonParser p, DeserializationContext ctxt, int depth)
                 values[ptr++] = value2;
                 int totalSize = ptr;
                 do {
    -                value = deserialize(p, ctxt, depth + 1);
    +                value = deserialize(p, ctxt, depth);
                     ++totalSize;
                     if (ptr >= values.length) {
                         values = buffer.appendCompletedChunk(values);
    @@ -914,11 +915,12 @@ protected Object mapArray(JsonParser p, DeserializationContext ctxt, int depth)
              * Method called to map a JSON Array into a Java Object array (Object[]).
              */
             protected Object[] mapArrayToArray(JsonParser p, DeserializationContext ctxt, int depth) throws IOException {
    +            ++depth;
                 ObjectBuffer buffer = ctxt.leaseObjectBuffer();
                 Object[] values = buffer.resetAndStart();
                 int ptr = 0;
                 do {
    -                Object value = deserialize(p, ctxt, depth + 1);
    +                Object value = deserialize(p, ctxt, depth);
                     if (ptr >= values.length) {
                         values = buffer.appendCompletedChunk(values);
                         ptr = 0;
    @@ -933,11 +935,12 @@ protected Object[] mapArrayToArray(JsonParser p, DeserializationContext ctxt, in
              */
             protected Object mapObject(JsonParser p, DeserializationContext ctxt, int depth) throws IOException
             {
    +            ++depth;
                 // 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, depth + 1);
    +            Object value1 = deserialize(p, ctxt, depth);
     
                 String key2 = p.nextFieldName();
                 if (key2 == null) { // single entry; but we want modifiable
    @@ -946,7 +949,7 @@ protected Object mapObject(JsonParser p, DeserializationContext ctxt, int depth)
                     return result;
                 }
                 p.nextToken();
    -            Object value2 = deserialize(p, ctxt, depth + 1);
    +            Object value2 = deserialize(p, ctxt, depth);
     
                 String key = p.nextFieldName();
                 if (key == null) {
    @@ -968,7 +971,7 @@ protected Object mapObject(JsonParser p, DeserializationContext ctxt, int depth)
     
                 do {
                     p.nextToken();
    -                final Object newValue = deserialize(p, ctxt, depth + 1);
    +                final Object newValue = deserialize(p, ctxt, depth);
                     final Object oldValue = result.put(key, newValue);
                     if (oldValue != null) {
                         return _mapObjectWithDups(p, ctxt, result, key, oldValue, newValue,
    
  • src/main/java/com/fasterxml/jackson/databind/module/SimpleModule.java+1 1 modified
    @@ -15,7 +15,7 @@
     import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
     
     /**
    - * Vanilla {@link Module} implementation that allows registration
    + * Vanilla {@link com.fasterxml.jackson.databind.Module} implementation that allows registration
      * of serializers and deserializers, bean serializer
      * and deserializer modifiers, registration of subtypes and mix-ins
      * as well as some other commonly
    
  • src/main/java/com/fasterxml/jackson/databind/PropertyNamingStrategies.java+3 1 modified
    @@ -265,7 +265,9 @@ public String translate(String input)
          * snake case conversion functionality offered by the strategy.
          * @since 2.13
          */
    -    public static class UpperSnakeCaseStrategy extends SnakeCaseStrategy {
    +    public static class UpperSnakeCaseStrategy extends SnakeCaseStrategy
    +    {
    +        private static final long serialVersionUID = 2L;
     
             @Override
             public String translate(String input) {
    
  • src/test/java/com/fasterxml/jackson/databind/deser/DeepNestingUntypedDeserTest.java+0 70 removed
    @@ -1,70 +0,0 @@
    -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();
    -  }
    -}
    
  • src/test/java/com/fasterxml/jackson/databind/deser/dos/DeepNestingUntypedDeserTest.java+73 0 added
    @@ -0,0 +1,73 @@
    +package com.fasterxml.jackson.databind.deser.dos;
    +
    +import java.util.List;
    +import java.util.Map;
    +
    +import com.fasterxml.jackson.core.exc.StreamReadException;
    +
    +import com.fasterxml.jackson.databind.BaseMapTest;
    +import com.fasterxml.jackson.databind.ObjectMapper;
    +
    +// For [databind#2816]
    +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 (StreamReadException e) {
    +            verifyException(e, "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 (StreamReadException e) {
    +            verifyException(e, "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();
    +    }
    +}
    
  • src/test-jdk14/java/com/fasterxml/jackson/databind/failing/RecordWithJsonNaming3102Test.java+1 6 modified
    @@ -1,17 +1,12 @@
     package com.fasterxml.jackson.databind.failing;
     
    -import java.util.List;
    -import java.util.Map;
    -
     import com.fasterxml.jackson.annotation.JsonCreator;
    -import com.fasterxml.jackson.annotation.JsonSetter;
    -import com.fasterxml.jackson.annotation.Nulls;
    +
     import com.fasterxml.jackson.databind.BaseMapTest;
     import com.fasterxml.jackson.databind.ObjectMapper;
     import com.fasterxml.jackson.databind.ObjectReader;
     import com.fasterxml.jackson.databind.PropertyNamingStrategies;
     import com.fasterxml.jackson.databind.annotation.JsonNaming;
    -import com.fasterxml.jackson.databind.exc.InvalidNullException;
     
     // [databind#3102]: fails on JDK 16 which finally blocks mutation
     // of Record fields.
    
  • src/test-jdk14/java/com/fasterxml/jackson/databind/records/RecordUpdate3079Test.java+0 1 modified
    @@ -1,7 +1,6 @@
     package com.fasterxml.jackson.databind.records;
     
     import java.util.Collections;
    -import java.util.Map;
     
     import com.fasterxml.jackson.databind.*;
     
    
fcfc4998ec23

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

https://github.com/FasterXML/jackson-databindTaylor S. MarksMar 22, 2022via ghsa
2 files changed · +133 47
  • src/main/java/com/fasterxml/jackson/databind/deser/std/UntypedObjectDeserializer.java+63 47 modified
    @@ -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
    @@ -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);
             }
    @@ -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);
    @@ -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);
    @@ -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;
    @@ -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
    @@ -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) {
    @@ -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,
    
  • src/test/java/com/fasterxml/jackson/databind/deser/DeepNestingUntypedDeserTest.java+70 0 added
    @@ -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();
    +  }
    +}
    
0a8157c6ca47

Bit more testing wrt #2816

https://github.com/FasterXML/jackson-databindTatu SalorantaAug 13, 2020via ghsa
1 file changed · +20 5
  • src/test/java/com/fasterxml/jackson/failing/DeepNestingDeser2816Test.java+20 5 renamed
    @@ -6,27 +6,42 @@
     import com.fasterxml.jackson.databind.*;
     
     // [databind#2816]
    -public class DeepNestingWithUntyped2816Test extends BaseMapTest
    +public class DeepNestingDeser2816Test extends BaseMapTest
     {
    -    // 2000 passes, 3000 fails
    -    private final static int TOO_DEEP_NESTING = 4000;
    +    // 2000 passes for all; 3000 fails for untyped, 5000 for tree/object,
    +    // 8000 for tree/array too
    +    private final static int TOO_DEEP_NESTING = 8000;
     
         private final ObjectMapper MAPPER = newJsonMapper();
     
    -    public void testWithArray() throws Exception
    +    public void testUntypedWithArray() throws Exception
         {
             final String doc = _nestedDoc(TOO_DEEP_NESTING, "[ ", "] ");
             Object ob = MAPPER.readValue(doc, Object.class);
             assertTrue(ob instanceof List<?>);
         }
     
    -    public void testWithObject() throws Exception
    +    public void testUntypedWithObject() throws Exception
         {
             final String doc = "{"+_nestedDoc(TOO_DEEP_NESTING, "\"x\":{", "} ") + "}";
             Object ob = MAPPER.readValue(doc, Object.class);
             assertTrue(ob instanceof Map<?,?>);
         }
     
    +    public void testTreeWithArray() throws Exception
    +    {
    +        final String doc = _nestedDoc(TOO_DEEP_NESTING, "[ ", "] ");
    +        JsonNode n = MAPPER.readTree(doc);
    +        assertTrue(n.isArray());
    +    }
    +
    +    public void testTreeWithObject() throws Exception
    +    {
    +        final String doc = "{"+_nestedDoc(TOO_DEEP_NESTING, "\"x\":{", "} ") + "}";
    +        JsonNode n = MAPPER.readTree(doc);
    +        assertTrue(n.isObject());
    +    }
    +    
         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) {
    
b3587924ee5d

Add failing test for #2816

https://github.com/FasterXML/jackson-databindTatu SalorantaAug 13, 2020via ghsa
1 file changed · +46 0
  • src/test/java/com/fasterxml/jackson/failing/DeepNestingWithUntyped2816Test.java+46 0 added
    @@ -0,0 +1,46 @@
    +package com.fasterxml.jackson.failing;
    +
    +import java.util.List;
    +import java.util.Map;
    +
    +import com.fasterxml.jackson.databind.*;
    +
    +// [databind#2816]
    +public class DeepNestingWithUntyped2816Test extends BaseMapTest
    +{
    +    // 2000 passes, 3000 fails
    +    private final static int TOO_DEEP_NESTING = 4000;
    +
    +    private final ObjectMapper MAPPER = newJsonMapper();
    +
    +    public void testWithArray() throws Exception
    +    {
    +        final String doc = _nestedDoc(TOO_DEEP_NESTING, "[ ", "] ");
    +        Object ob = MAPPER.readValue(doc, Object.class);
    +        assertTrue(ob instanceof List<?>);
    +    }
    +
    +    public void testWithObject() throws Exception
    +    {
    +        final String doc = "{"+_nestedDoc(TOO_DEEP_NESTING, "\"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();
    +    }
    +}
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

15

News mentions

0

No linked articles in our index yet.