CVE-2018-21234: Deserialization of Untrusted Data (CWE-502)
value: Jodd before 5.0.4 performs Deserialization of Untrusted JSON Data when setClassMetadataName is set.
CWE description: The product deserializes untrusted data without sufficiently verifying that the resulting data will be valid.
CVSS v3 base score: 9.8
Published: 2020-05-21T23:15Z
Repository: https://github.com/oblac/jodd
Language: Java
Files changed: CHANGELOG.md, jodd-json/src/main/java/jodd/json/JsonParser.java, jodd-json/src/main/java/jodd/json/JsonParserBase.java, jodd-json/src/main/java/jodd/json/MapToBean.java, jodd-json/src/test/java/jodd/json/JSONDeserializerTest.java
Commit date: 2018-08-17 18:32:20 +0200
Commit message: Added `allowClass` (closes #628)

Diff: diff --git a/CHANGELOG.md b/CHANGELOG.md
index 186210faa..4b77ee175 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,10 +22,11 @@ All notable changes to Jodd project are documented here.
 
 + **joy** - added excluded jars for faster scanning.
 + **mail** - added custom properties.
 + **json** - added `onValue` callback for JSON serializer.
 + **json** - added `excludeEmpty` flag for JSON serializer.
++ **json** - added `allowClass` for whitelisting class names.
 + **petite** - allow injection in the private fields of super types.
 
 
 
 ## [v5.0.3](https://github.com/oblac/jodd/compare/v5.0.2...v5.0.3)
diff --git a/jodd-json/src/main/java/jodd/json/JsonParser.java b/jodd-json/src/main/java/jodd/json/JsonParser.java
index 95ff9b848..dc6d408f2 100644
--- a/jodd-json/src/main/java/jodd/json/JsonParser.java
+++ b/jodd-json/src/main/java/jodd/json/JsonParser.java
@@ -34,10 +34,11 @@ import jodd.util.CharArraySequence;
 import jodd.util.CharUtil;
 import jodd.util.StringPool;
 import jodd.util.UnsafeUtil;
 
 import java.math.BigInteger;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.function.Supplier;
@@ -305,10 +306,30 @@ public class JsonParser extends JsonParserBase {
 			classMetadataName = null;
 		}
 		return this;
 	}
 
+	/**
+	 * Adds a {@link jodd.util.Wildcard wildcard} pattern for white-listing classes.
+	 * @see #setClassMetadataName(String)
+	 */
+	public JsonParser allowClass(final String classPattern) {
+		if (super.classnameWhitelist == null) {
+			super.classnameWhitelist = new ArrayList<>();
+		}
+		classnameWhitelist.add(classPattern);
+		return this;
+	}
+
+	/**
+	 * Removes the whitelist of allowed classes.
+	 * @see #setClassMetadataName(String)
+	 */
+	public JsonParser allowAllClasses() {
+		classnameWhitelist = null;
+		return this;
+	}
 
 	// ---------------------------------------------------------------- parse
 
 	/**
 	 * Parses input JSON as given type.
diff --git a/jodd-json/src/main/java/jodd/json/JsonParserBase.java b/jodd-json/src/main/java/jodd/json/JsonParserBase.java
index 5f9ba42bc..0a4cccda0 100644
--- a/jodd-json/src/main/java/jodd/json/JsonParserBase.java
+++ b/jodd-json/src/main/java/jodd/json/JsonParserBase.java
@@ -53,10 +53,11 @@ public abstract class JsonParserBase {
 	protected static final Supplier<List> ARRAYLIST_SUPPLIER = ArrayList::new;
 	protected static final Supplier<List> LAZYLIST_SUPPLIER = LazyList::new;
 
 	protected Supplier<Map> mapSupplier = HASMAP_SUPPLIER;
 	protected Supplier<List> listSupplier = ARRAYLIST_SUPPLIER;
+	protected List<String> classnameWhitelist;
 
 	/**
 	 * Creates new instance of {@link jodd.json.MapToBean}.
 	 */
 	protected MapToBean createMapToBean(final String classMetadataName) {
diff --git a/jodd-json/src/main/java/jodd/json/MapToBean.java b/jodd-json/src/main/java/jodd/json/MapToBean.java
index d0472a5e4..dfd8bb2cf 100644
--- a/jodd-json/src/main/java/jodd/json/MapToBean.java
+++ b/jodd-json/src/main/java/jodd/json/MapToBean.java
@@ -30,10 +30,11 @@ import jodd.introspector.ClassIntrospector;
 import jodd.introspector.PropertyDescriptor;
 import jodd.introspector.Setter;
 import jodd.typeconverter.TypeConverterManager;
 import jodd.util.ClassLoaderUtil;
 import jodd.util.ClassUtil;
+import jodd.util.Wildcard;
 
 import java.lang.reflect.InvocationTargetException;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -67,10 +68,12 @@ public class MapToBean {
 				// nothing to do, no information about target type found
 				target = map;
 			}
 		}
 		else {
+			checkClassName(jsonParser.classnameWhitelist, className);
+
 			try {
 				targetType = ClassLoaderUtil.loadClass(className);
 			} catch (ClassNotFoundException cnfex) {
 				throw new JsonException(cnfex);
 			}
@@ -143,10 +146,21 @@ public class MapToBean {
 		}
 
 		return target;
 	}
 
+	private void checkClassName(final List<String> classnameWhitelist, final String className) {
+		if (classnameWhitelist == null) {
+			return;
+		}
+		classnameWhitelist.forEach(pattern -> {
+			if (!Wildcard.equalsOrMatch(className, pattern)) {
+				throw new JsonException("Class can't be loaded as it is not whitelisted: " + className);
+			}
+		});
+	}
+
 	/**
 	 * Converts type of all list elements to match the component type.
 	 */
 	private Object generifyList(final List list, final Class componentType) {
 		for (int i = 0; i < list.size(); i++) {
diff --git a/jodd-json/src/test/java/jodd/json/JSONDeserializerTest.java b/jodd-json/src/test/java/jodd/json/JSONDeserializerTest.java
index 7eb48a16c..8301bc0e5 100644
--- a/jodd-json/src/test/java/jodd/json/JSONDeserializerTest.java
+++ b/jodd-json/src/test/java/jodd/json/JSONDeserializerTest.java
@@ -60,10 +60,11 @@ import java.util.TimeZone;
 
 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 
 class JSONDeserializerTest {
 
@@ -707,10 +708,41 @@ class JSONDeserializerTest {
 			assertEquals(1.0f, point.x, DELTA);
 			assertEquals(2.0f, point.y, DELTA);
 		});
 	}
 
+	@Test
+	void testPointWithException() {
+		JsonParser.Defaults.classMetadataName = "__class";
+		JsonSerializer.Defaults.classMetadataName = "__class";
+
+		JsonParsers.forEachParser(jsonParser -> {
+			jsonParser.allowClass("notAllowed");
+			final String json = new JsonSerializer().serialize(new Point2D.Float(1.0f, 2.0f));
+			assertThrows(JsonException.class, () -> {
+				jsonParser.parse(json);
+			});
+			jsonParser.allowAllClasses();
+		});
+	}
+
+	@Test
+	void testPointWithoutExceptionWhitelisted() {
+		JsonParser.Defaults.classMetadataName = "__class";
+		JsonSerializer.Defaults.classMetadataName = "__class";
+
+		JsonParsers.forEachParser(jsonParser -> {
+			jsonParser.allowClass("*.Point?D*");
+			String json = new JsonSerializer().serialize(new Point2D.Float(1.0f, 2.0f));
+			Point2D.Float point = jsonParser.parse(json);
+			assertEquals(1.0f, point.x, DELTA);
+			assertEquals(2.0f, point.y, DELTA);
+			jsonParser.allowAllClasses();
+		});
+	}
+
+
 	@Test
 	void testUnixEpoch() {
 		JsonParsers.forEachParser(jsonParser -> {
 			Calendar cal = Calendar.getInstance();
 			cal.setTimeZone(TimeZone.getTimeZone("GMT"));

Fixed code: + **json** - added `allowClass` for whitelisting class names.
import java.util.ArrayList;
	/**
	 * Adds a {@link jodd.util.Wildcard wildcard} pattern for white-listing classes.
	 * @see #setClassMetadataName(String)
	 */
	public JsonParser allowClass(final String classPattern) {
		if (super.classnameWhitelist == null) {
			super.classnameWhitelist = new ArrayList<>();
		}
		classnameWhitelist.add(classPattern);
		return this;
	}

	/**
	 * Removes the whitelist of allowed classes.
	 * @see #setClassMetadataName(String)
	 */
	public JsonParser allowAllClasses() {
		classnameWhitelist = null;
		return this;
	}
	protected List<String> classnameWhitelist;
import jodd.util.Wildcard;
			checkClassName(jsonParser.classnameWhitelist, className);

	private void checkClassName(final List<String> classnameWhitelist, final String className) {
		if (classnameWhitelist == null) {
			return;
		}
		classnameWhitelist.forEach(pattern -> {
			if (!Wildcard.equalsOrMatch(className, pattern)) {
				throw new JsonException("Class can't be loaded as it is not whitelisted: " + className);
			}
		});
	}

import static org.junit.jupiter.api.Assertions.assertThrows;
	@Test
	void testPointWithException() {
		JsonParser.Defaults.classMetadataName = "__class";
		JsonSerializer.Defaults.classMetadataName = "__class";

		JsonParsers.forEachParser(jsonParser -> {
			jsonParser.allowClass("notAllowed");
			final String json = new JsonSerializer().serialize(new Point2D.Float(1.0f, 2.0f));
			assertThrows(JsonException.class, () -> {
				jsonParser.parse(json);
			});
			jsonParser.allowAllClasses();
		});
	}

	@Test
	void testPointWithoutExceptionWhitelisted() {
		JsonParser.Defaults.classMetadataName = "__class";
		JsonSerializer.Defaults.classMetadataName = "__class";

		JsonParsers.forEachParser(jsonParser -> {
			jsonParser.allowClass("*.Point?D*");
			String json = new JsonSerializer().serialize(new Point2D.Float(1.0f, 2.0f));
			Point2D.Float point = jsonParser.parse(json);
			assertEquals(1.0f, point.x, DELTA);
			assertEquals(2.0f, point.y, DELTA);
			jsonParser.allowAllClasses();
		});
	}