From: Markus Koschany <apo@debian.org>
Date: Tue, 15 Nov 2022 12:51:57 +0100
Subject: CVE-2020-36518

Bug-Debian: https://bugs.debian.org/1007109
Origin: https://github.com/FasterXML/jackson-databind/commit/83b928dab9ba6ef81cf48987fcd12071e1ddb0c9
Origin: https://github.com/FasterXML/jackson-databind/commit/fcfc4998ec23f0b1f7f8a9521c2b317b6c25892b
---
 .../deser/std/UntypedObjectDeserializer.java       | 140 +++++++++++----------
 .../deser/DeepNestingUntypedDeserTest.java         |  70 +++++++++++
 2 files changed, 147 insertions(+), 63 deletions(-)
 create mode 100644 src/test/java/com/fasterxml/jackson/databind/deser/DeepNestingUntypedDeserTest.java

diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/UntypedObjectDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/UntypedObjectDeserializer.java
index 67be238..41f6dd9 100644
--- a/src/main/java/com/fasterxml/jackson/databind/deser/std/UntypedObjectDeserializer.java
+++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/UntypedObjectDeserializer.java
@@ -220,10 +220,9 @@ public class UntypedObjectDeserializer
      */
     @Override
     public boolean isCachable() {
-        /* 26-Mar-2015, tatu: With respect to [databind#735], there are concerns over
-         *   cachability. It seems like we SHOULD be safe here; but just in case there
-         *   are problems with false sharing, this may need to be revisited.
-         */
+        // 26-Mar-2015, tatu: With respect to [databind#735], there are concerns over
+        //   cachability. It seems like we SHOULD be safe here; but just in case there
+        //   are problems with false sharing, this may need to be revisited.
         return true;
     }
 
@@ -266,9 +265,8 @@ public class UntypedObjectDeserializer
             if (_numberDeserializer != null) {
                 return _numberDeserializer.deserialize(p, ctxt);
             }
-            /* Caller may want to get all integral values returned as {@link java.math.BigInteger},
-             * or {@link java.lang.Long} for consistency
-             */
+            // Caller may want to get all integral values returned as {@link java.math.BigInteger},
+            // or {@link java.lang.Long} for consistency
             if (ctxt.hasSomeOfFeatures(F_MASK_INT_COERCIONS)) {
                 return _coerceIntegral(p, ctxt);
             }
@@ -599,10 +597,9 @@ public class UntypedObjectDeserializer
     }
 
     /*
-    /**********************************************************
-    /* Separate "vanilla" implementation for common case of
-    /* no custom deserializer overrides
-    /**********************************************************
+    /**********************************************************************
+    /* Separate "vanilla" implementation for common case of no deser overrides
+    /**********************************************************************
      */
 
     @JacksonStdImpl
@@ -611,11 +608,13 @@ public class UntypedObjectDeserializer
     {
         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
-         */
+        // @since 2.9
         protected final boolean _nonMerging;
         
         public Vanilla() { this(false); }
@@ -639,65 +638,77 @@ public class UntypedObjectDeserializer
             return _nonMerging ? Boolean.FALSE : null;
         }
 
-        @Override
-        public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException
+         @Override
+        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.getCurrentTokenId()) {
-            case JsonTokenId.ID_START_OBJECT:
-                {
+            switch (p.currentTokenId()) {
+                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);
         }
@@ -806,15 +817,16 @@ public class UntypedObjectDeserializer
             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);
+            ++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);
+            Object value2 = deserialize(p, ctxt, depth);
             if (p.nextToken()  == JsonToken.END_ARRAY) {
                 ArrayList<Object> l = new ArrayList<Object>(2);
                 l.add(value);
@@ -828,7 +840,7 @@ public class UntypedObjectDeserializer
             values[ptr++] = value2;
             int totalSize = ptr;
             do {
-                value = deserialize(p, ctxt);
+                value = deserialize(p, ctxt, depth);
                 ++totalSize;
                 if (ptr >= values.length) {
                     values = buffer.appendCompletedChunk(values);
@@ -845,12 +857,13 @@ public class UntypedObjectDeserializer
         /**
          * 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 {
+            ++depth;
             ObjectBuffer buffer = ctxt.leaseObjectBuffer();
             Object[] values = buffer.resetAndStart();
             int ptr = 0;
             do {
-                Object value = deserialize(p, ctxt);
+                Object value = deserialize(p, ctxt, depth);
                 if (ptr >= values.length) {
                     values = buffer.appendCompletedChunk(values);
                     ptr = 0;
@@ -863,12 +876,13 @@ public class UntypedObjectDeserializer
         /**
          * 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
         {
+            ++depth;
             // will point to FIELD_NAME at this point, guaranteed
             String key1 = p.getText();
             p.nextToken();
-            Object value1 = deserialize(p, ctxt);
+            Object value1 = deserialize(p, ctxt, depth);
 
             String key2 = p.nextFieldName();
             if (key2 == null) { // single entry; but we want modifiable
@@ -877,7 +891,7 @@ public class UntypedObjectDeserializer
                 return result;
             }
             p.nextToken();
-            Object value2 = deserialize(p, ctxt);
+            Object value2 = deserialize(p, ctxt, depth);
 
             String key = p.nextFieldName();
             if (key == null) {
@@ -892,7 +906,7 @@ public class UntypedObjectDeserializer
             result.put(key2, value2);
             do {
                 p.nextToken();
-                result.put(key, deserialize(p, ctxt));
+                result.put(key, deserialize(p, ctxt, depth));
             } while ((key = p.nextFieldName()) != null);
             return result;
         }
diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/DeepNestingUntypedDeserTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/DeepNestingUntypedDeserTest.java
new file mode 100644
index 0000000..ad0194d
--- /dev/null
+++ b/src/test/java/com/fasterxml/jackson/databind/deser/DeepNestingUntypedDeserTest.java
@@ -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 = new ObjectMapper();
+
+  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();
+  }
+}
