CVE-2022-42003: Deserialization of Untrusted Data (CWE-502)
value: In FasterXML jackson-databind before versions 2.13.4.1 and 2.12.17.1, resource exhaustion can occur because of a lack of a check in primitive value deserializers to avoid deep wrapper array nesting, when the UNWRAP_SINGLE_VALUE_ARRAYS feature is enabled.
CWE description: The product deserializes untrusted data without sufficiently verifying that the resulting data will be valid.
CVSS v3 base score: 7.5
Published: 2022-10-02T05:15Z
Repository: https://github.com/FasterXML/jackson-databind
Language: Java
Files changed: release-notes/VERSION-2.x, src/main/java/com/fasterxml/jackson/databind/deser/std/StdDeserializer.java, src/test/java/com/fasterxml/jackson/databind/deser/dos/DeepArrayWrappingForDeser3590Test.java
Commit date: 2022-09-06 17:05:53 -0700
Commit message: Fix #3590

Diff: diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x
index 0ebde3226a..3d76ef7da7 100644
--- a/release-notes/VERSION-2.x
+++ b/release-notes/VERSION-2.x
@@ -55,10 +55,12 @@ Project: jackson-databind
   custom `NullValueProvider`
 #3535: Replace `JsonNode.with()` with `JsonNode.withObject()`
 #3559: Support `null`-valued `Map` fields with "any setter"
 #3568: Change `JsonNode.with(String)` and `withArray(String)` to consider
   argument as `JsonPointer` if valid expression
+#3590: Add check in primitive value deserializers to avoid deep wrapper array
+  nesting wrt `UNWRAP_SINGLE_VALUE_ARRAYS`
 
 2.13.4 (03-Sep-2022)
 
 #3275: JDK 16 Illegal reflective access for `Throwable.setCause()` with
   `PropertyNamingStrategy.UPPER_CAMEL_CASE`
diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/StdDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/StdDeserializer.java
index 50284cd125..7e8b111ef8 100644
--- a/src/main/java/com/fasterxml/jackson/databind/deser/std/StdDeserializer.java
+++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/StdDeserializer.java
@@ -355,16 +355,12 @@ public abstract class StdDeserializer<T>
     protected T _deserializeWrappedValue(JsonParser p, DeserializationContext ctxt) throws IOException
     {
         // 23-Mar-2017, tatu: Let's specifically block recursive resolution to avoid
         //   either supporting nested arrays, or to cause infinite looping.
         if (p.hasToken(JsonToken.START_ARRAY)) {
-            String msg = String.format(
-"Cannot deserialize instance of %s out of %s token: nested Arrays not allowed with %s",
-                    ClassUtil.nameOf(_valueClass), JsonToken.START_ARRAY,
-                    "DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS");
             @SuppressWarnings("unchecked")
-            T result = (T) ctxt.handleUnexpectedToken(getValueType(ctxt), p.currentToken(), p, msg);
+            T result = (T) handleNestedArrayForSingle(p, ctxt);
             return result;
         }
         return (T) deserialize(p, ctxt);
     }
 
@@ -411,11 +407,13 @@ public abstract class StdDeserializer<T>
             text = ctxt.extractScalarFromObject(p, this, Boolean.TYPE);
             break;
         case JsonTokenId.ID_START_ARRAY:
             // 12-Jun-2020, tatu: For some reason calling `_deserializeFromArray()` won't work so:
             if (ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) {
-                p.nextToken();
+                if (p.nextToken() == JsonToken.START_ARRAY) {
+                    return (boolean) handleNestedArrayForSingle(p, ctxt);
+                }
                 final boolean parsed = _parseBooleanPrimitive(p, ctxt);
                 _verifyEndArrayForSingle(p, ctxt);
                 return parsed;
             }
             // fall through
@@ -580,11 +578,13 @@ public abstract class StdDeserializer<T>
             text = ctxt.extractScalarFromObject(p, this, Byte.TYPE);
             break;
         case JsonTokenId.ID_START_ARRAY: // unwrapping / from-empty-array coercion?
             // 12-Jun-2020, tatu: For some reason calling `_deserializeFromArray()` won't work so:
             if (ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) {
-                p.nextToken();
+                if (p.nextToken() == JsonToken.START_ARRAY) {
+                    return (byte) handleNestedArrayForSingle(p, ctxt);
+                }
                 final byte parsed = _parseBytePrimitive(p, ctxt);
                 _verifyEndArrayForSingle(p, ctxt);
                 return parsed;
             }
             // fall through
@@ -650,11 +650,13 @@ public abstract class StdDeserializer<T>
             text = ctxt.extractScalarFromObject(p, this, Short.TYPE);
             break;
         case JsonTokenId.ID_START_ARRAY:
             // 12-Jun-2020, tatu: For some reason calling `_deserializeFromArray()` won't work so:
             if (ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) {
-                p.nextToken();
+                if (p.nextToken() == JsonToken.START_ARRAY) {
+                    return (short) handleNestedArrayForSingle(p, ctxt);
+                }
                 final short parsed = _parseShortPrimitive(p, ctxt);
                 _verifyEndArrayForSingle(p, ctxt);
                 return parsed;
             }
             // fall through to fail
@@ -717,11 +719,13 @@ public abstract class StdDeserializer<T>
         case JsonTokenId.ID_START_OBJECT:
             text = ctxt.extractScalarFromObject(p, this, Integer.TYPE);
             break;
         case JsonTokenId.ID_START_ARRAY:
             if (ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) {
-                p.nextToken();
+                if (p.nextToken() == JsonToken.START_ARRAY) {
+                    return (int) handleNestedArrayForSingle(p, ctxt);
+                }
                 final int parsed = _parseIntPrimitive(p, ctxt);
                 _verifyEndArrayForSingle(p, ctxt);
                 return parsed;
             }
             // fall through to fail
@@ -868,11 +872,13 @@ public abstract class StdDeserializer<T>
         case JsonTokenId.ID_START_OBJECT:
             text = ctxt.extractScalarFromObject(p, this, Long.TYPE);
             break;
         case JsonTokenId.ID_START_ARRAY:
             if (ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) {
-                p.nextToken();
+                if (p.nextToken() == JsonToken.START_ARRAY) {
+                    return (long) handleNestedArrayForSingle(p, ctxt);
+                }
                 final long parsed = _parseLongPrimitive(p, ctxt);
                 _verifyEndArrayForSingle(p, ctxt);
                 return parsed;
             }
             // fall through
@@ -1001,11 +1007,13 @@ public abstract class StdDeserializer<T>
         case JsonTokenId.ID_START_OBJECT:
             text = ctxt.extractScalarFromObject(p, this, Float.TYPE);
             break;
         case JsonTokenId.ID_START_ARRAY:
             if (ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) {
-                p.nextToken();
+                if (p.nextToken() == JsonToken.START_ARRAY) {
+                    return (float) handleNestedArrayForSingle(p, ctxt);
+                }
                 final float parsed = _parseFloatPrimitive(p, ctxt);
                 _verifyEndArrayForSingle(p, ctxt);
                 return parsed;
             }
             // fall through
@@ -1130,11 +1138,13 @@ public abstract class StdDeserializer<T>
         case JsonTokenId.ID_START_OBJECT:
             text = ctxt.extractScalarFromObject(p, this, Double.TYPE);
             break;
         case JsonTokenId.ID_START_ARRAY:
             if (ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) {
-                p.nextToken();
+                if (p.nextToken() == JsonToken.START_ARRAY) {
+                    return (double) handleNestedArrayForSingle(p, ctxt);
+                }
                 final double parsed = _parseDoublePrimitive(p, ctxt);
                 _verifyEndArrayForSingle(p, ctxt);
                 return parsed;
             }
             // fall through
@@ -1311,10 +1321,13 @@ public abstract class StdDeserializer<T>
                 case TryConvert:
                     return (java.util.Date) getNullValue(ctxt);
                 default:
                 }
             } else if (unwrap) {
+                if (t == JsonToken.START_ARRAY) {
+                    return (java.util.Date) handleNestedArrayForSingle(p, ctxt);
+                }
                 final Date parsed = _parseDate(p, ctxt);
                 _verifyEndArrayForSingle(p, ctxt);
                 return parsed;
             }
         }
@@ -2107,10 +2120,25 @@ inputDesc, _coercedTypeDesc());
 handledType().getName());
         // 05-May-2016, tatu: Should recover somehow (maybe skip until END_ARRAY);
         //     but for now just fall through
     }
 
+    /**
+     * Helper method called when detecting a deep(er) nesting of Arrays when trying
+     * to unwrap value for {@code DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS}.
+     *
+     * @since 2.14
+     */
+    protected Object handleNestedArrayForSingle(JsonParser p, DeserializationContext ctxt) throws IOException
+    {
+        String msg = String.format(
+"Cannot deserialize instance of %s out of %s token: nested Arrays not allowed with %s",
+                ClassUtil.nameOf(_valueClass), JsonToken.START_ARRAY,
+                "DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS");
+        return ctxt.handleUnexpectedToken(getValueType(ctxt), p.currentToken(), p, msg);
+    }
+
     protected void _verifyEndArrayForSingle(JsonParser p, DeserializationContext ctxt) throws IOException
     {
         JsonToken t = p.nextToken();
         if (t != JsonToken.END_ARRAY) {
             handleMissingEndArrayForSingle(p, ctxt);
diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/dos/DeepArrayWrappingForDeser3590Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/dos/DeepArrayWrappingForDeser3590Test.java
new file mode 100644
index 0000000000..e5b0f1eaf3
--- /dev/null
+++ b/src/test/java/com/fasterxml/jackson/databind/deser/dos/DeepArrayWrappingForDeser3590Test.java
@@ -0,0 +1,95 @@
+package com.fasterxml.jackson.databind.deser.dos;
+
+import java.util.Date;
+
+import com.fasterxml.jackson.databind.*;
+import com.fasterxml.jackson.databind.exc.MismatchedInputException;
+
+public class DeepArrayWrappingForDeser3590Test extends BaseMapTest
+{
+    // 05-Sep-2022, tatu: Before fix, failed with 5000
+    private final static int TOO_DEEP_NESTING = 9999;
+
+    private final ObjectMapper MAPPER = jsonMapperBuilder()
+            .enable(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
+            .build();
+
+    private final static String TOO_DEEP_DOC = _nestedDoc(TOO_DEEP_NESTING, "[ ", "] ", "123");
+
+    public void testArrayWrappingForBoolean() throws Exception
+    {
+        _testArrayWrappingFor(Boolean.class);
+        _testArrayWrappingFor(Boolean.TYPE);
+    }
+
+    public void testArrayWrappingForByte() throws Exception
+    {
+        _testArrayWrappingFor(Byte.class);
+        _testArrayWrappingFor(Byte.TYPE);
+    }
+
+    public void testArrayWrappingForShort() throws Exception
+    {
+        _testArrayWrappingFor(Short.class);
+        _testArrayWrappingFor(Short.TYPE);
+    }
+
+    public void testArrayWrappingForInt() throws Exception
+    {
+        _testArrayWrappingFor(Integer.class);
+        _testArrayWrappingFor(Integer.TYPE);
+    }
+
+    public void testArrayWrappingForLong() throws Exception
+    {
+        _testArrayWrappingFor(Long.class);
+        _testArrayWrappingFor(Long.TYPE);
+    }
+
+    public void testArrayWrappingForFloat() throws Exception
+    {
+        _testArrayWrappingFor(Float.class);
+        _testArrayWrappingFor(Float.TYPE);
+    }
+
+    public void testArrayWrappingForDouble() throws Exception
+    {
+        _testArrayWrappingFor(Double.class);
+        _testArrayWrappingFor(Double.TYPE);
+    }
+
+    public void testArrayWrappingForDate() throws Exception
+    {
+        _testArrayWrappingFor(Date.class);
+    }
+
+    private void _testArrayWrappingFor(Class<?> cls) throws Exception
+    {
+        try {
+            MAPPER.readValue(TOO_DEEP_DOC, cls);
+            fail("Should not pass");
+        } catch (MismatchedInputException e) {
+            verifyException(e, "Cannot deserialize");
+            verifyException(e, "nested Arrays not allowed");
+        }
+    }
+
+    private static String _nestedDoc(int nesting, String open, String close, String content) {
+        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");
+            }
+        }
+        sb.append("\n").append(content).append("\n");
+        for (int i = 0; i < nesting; ++i) {
+            sb.append(close);
+            if ((i & 31) == 0) {
+                sb.append("\n");
+            }
+        }
+        return sb.toString();
+    }
+
+}

Vulnerable code:             String msg = String.format(
"Cannot deserialize instance of %s out of %s token: nested Arrays not allowed with %s",
                    ClassUtil.nameOf(_valueClass), JsonToken.START_ARRAY,
                    "DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS");
            T result = (T) ctxt.handleUnexpectedToken(getValueType(ctxt), p.currentToken(), p, msg);
                p.nextToken();
                p.nextToken();
                p.nextToken();
                p.nextToken();
                p.nextToken();
                p.nextToken();
                p.nextToken();
Fixed code: #3590: Add check in primitive value deserializers to avoid deep wrapper array
  nesting wrt `UNWRAP_SINGLE_VALUE_ARRAYS`
            T result = (T) handleNestedArrayForSingle(p, ctxt);
                if (p.nextToken() == JsonToken.START_ARRAY) {
                    return (boolean) handleNestedArrayForSingle(p, ctxt);
                }
                if (p.nextToken() == JsonToken.START_ARRAY) {
                    return (byte) handleNestedArrayForSingle(p, ctxt);
                }
                if (p.nextToken() == JsonToken.START_ARRAY) {
                    return (short) handleNestedArrayForSingle(p, ctxt);
                }
                if (p.nextToken() == JsonToken.START_ARRAY) {
                    return (int) handleNestedArrayForSingle(p, ctxt);
                }
                if (p.nextToken() == JsonToken.START_ARRAY) {
                    return (long) handleNestedArrayForSingle(p, ctxt);
                }
                if (p.nextToken() == JsonToken.START_ARRAY) {
                    return (float) handleNestedArrayForSingle(p, ctxt);
                }
                if (p.nextToken() == JsonToken.START_ARRAY) {
                    return (double) handleNestedArrayForSingle(p, ctxt);
                }
                if (t == JsonToken.START_ARRAY) {
                    return (java.util.Date) handleNestedArrayForSingle(p, ctxt);
                }
    /**
     * Helper method called when detecting a deep(er) nesting of Arrays when trying
     * to unwrap value for {@code DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS}.
     *
     * @since 2.14
     */
    protected Object handleNestedArrayForSingle(JsonParser p, DeserializationContext ctxt) throws IOException
    {
        String msg = String.format(
"Cannot deserialize instance of %s out of %s token: nested Arrays not allowed with %s",
                ClassUtil.nameOf(_valueClass), JsonToken.START_ARRAY,
                "DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS");
        return ctxt.handleUnexpectedToken(getValueType(ctxt), p.currentToken(), p, msg);
    }

package com.fasterxml.jackson.databind.deser.dos;

import java.util.Date;

import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;

public class DeepArrayWrappingForDeser3590Test extends BaseMapTest
{
    // 05-Sep-2022, tatu: Before fix, failed with 5000
    private final static int TOO_DEEP_NESTING = 9999;

    private final ObjectMapper MAPPER = jsonMapperBuilder()
            .enable(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
            .build();

    private final static String TOO_DEEP_DOC = _nestedDoc(TOO_DEEP_NESTING, "[ ", "] ", "123");

    public void testArrayWrappingForBoolean() throws Exception
    {
        _testArrayWrappingFor(Boolean.class);
        _testArrayWrappingFor(Boolean.TYPE);
    }

    public void testArrayWrappingForByte() throws Exception
    {
        _testArrayWrappingFor(Byte.class);
        _testArrayWrappingFor(Byte.TYPE);
    }

    public void testArrayWrappingForShort() throws Exception
    {
        _testArrayWrappingFor(Short.class);
        _testArrayWrappingFor(Short.TYPE);
    }

    public void testArrayWrappingForInt() throws Exception
    {
        _testArrayWrappingFor(Integer.class);
        _testArrayWrappingFor(Integer.TYPE);
    }

    public void testArrayWrappingForLong() throws Exception
    {
        _testArrayWrappingFor(Long.class);
        _testArrayWrappingFor(Long.TYPE);
    }

    public void testArrayWrappingForFloat() throws Exception
    {
        _testArrayWrappingFor(Float.class);
        _testArrayWrappingFor(Float.TYPE);
    }

    public void testArrayWrappingForDouble() throws Exception
    {
        _testArrayWrappingFor(Double.class);
        _testArrayWrappingFor(Double.TYPE);
    }

    public void testArrayWrappingForDate() throws Exception
    {
        _testArrayWrappingFor(Date.class);
    }

    private void _testArrayWrappingFor(Class<?> cls) throws Exception
    {
        try {
            MAPPER.readValue(TOO_DEEP_DOC, cls);
            fail("Should not pass");
        } catch (MismatchedInputException e) {
            verifyException(e, "Cannot deserialize");
            verifyException(e, "nested Arrays not allowed");
        }
    }

    private static String _nestedDoc(int nesting, String open, String close, String content) {
        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");
            }
        }
        sb.append("\n").append(content).append("\n");
        for (int i = 0; i < nesting; ++i) {
            sb.append(close);
            if ((i & 31) == 0) {
                sb.append("\n");
            }
        }
        return sb.toString();
    }

}