347 lines
15 KiB
Diff
347 lines
15 KiB
Diff
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();
|
|
+ }
|
|
+}
|