GenericTypeIdParameterValidationTest.java
package tools.jackson.databind.jsontype.vld;
import java.util.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import tools.jackson.databind.DatabindContext;
import tools.jackson.databind.JavaType;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.DefaultTyping;
import tools.jackson.databind.annotation.JsonDeserialize;
import tools.jackson.databind.exc.InvalidDefinitionException;
import tools.jackson.databind.exc.InvalidTypeIdException;
import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
import tools.jackson.databind.jsontype.PolymorphicTypeValidator;
import tools.jackson.databind.jsontype.PolymorphicTypeValidator.Validity;
import tools.jackson.databind.testutil.DatabindTestUtil;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests for [CVE-2026-110879]: classes named as type parameters of a generic
* type id must pass the same checks as classes named directly as a type id
* (for base type {@code Object}), regardless of which declared type parameter
* they bind to and regardless of the outer type having been approved.
*/
public class GenericTypeIdParameterValidationTest extends DatabindTestUtil
{
static final List<String> INSTANTIATIONS = new ArrayList<>();
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
public interface Msg { }
public static class Env<T> implements Msg {
public T payload;
}
public static class PlainMsg implements Msg {
public String text;
}
/** Stand-in "gadget" type: never allow-listed */
public static class Gadget {
public Gadget() {
INSTANTIATIONS.add("Gadget()");
}
// Single-String constructor: usable as Map key via key deserializer
public Gadget(String key) {
INSTANTIATIONS.add("Gadget(" + key + ")");
}
public void setCmd(String cmd) {
INSTANTIATIONS.add("setCmd(" + cmd + ")");
}
}
/** Abstract, but still deserializable "gadget" type */
@JsonDeserialize(as = AbstractGadgetImpl.class)
public static abstract class AbstractGadget {
public void setCmd(String cmd) {
INSTANTIATIONS.add("setCmd(" + cmd + ")");
}
}
public static class AbstractGadgetImpl extends AbstractGadget {
public AbstractGadgetImpl() {
INSTANTIATIONS.add("AbstractGadgetImpl()");
}
}
/** Allow-listed (by some tests) bean type */
public static class SafeBean {
public String data;
}
public enum Color { RED, GREEN }
static class MsgWrapper {
public Msg msg;
}
static class MinimalClassWrapper {
@JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS)
public Iterable<String> values;
}
static final class StringMapWrapper {
public Map<String, String> map;
}
static final class ArrayMapWrapper {
public Map<String, Object> map;
}
static class SerializableWrapper {
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
public java.io.Serializable value;
}
public static class SubBean extends SafeBean {
public int extra;
}
public static final class FinalSubBean extends SafeBean {
public int extra;
}
static final class BeanArrayWrapper {
public SafeBean[] beans;
}
static class ObjectValueWrapper {
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
public Object value;
}
static final class ObjectArrayWrapper {
public Object[] values;
}
static final class EnumContainersWrapper {
public EnumMap<Color, String> enumMap;
public EnumSet<Color> enumSet;
}
private final String ENV = Env.class.getName();
private final String GADGET = Gadget.class.getName();
private final String ABSTRACT_GADGET = AbstractGadget.class.getName();
@BeforeEach
public void clearInstantiations() {
INSTANTIATIONS.clear();
}
/*
/**********************************************************************
/* Default (base-type limiting) validator, annotation-based typing
/**********************************************************************
*/
private ObjectMapper blockingMapper() {
// default validator in 3.x
return jsonMapperBuilder()
.build();
}
@Test
public void directGadgetIsNotASubtype() throws Exception
{
InvalidTypeIdException e = assertThrows(InvalidTypeIdException.class,
() -> blockingMapper().readValue(msgJson(GADGET, "\"cmd\":\"x\""),
MsgWrapper.class));
verifyException(e, "Not a subtype");
assertEquals(Collections.emptyList(), INSTANTIATIONS);
}
@Test
public void gadgetAsTypeParameterRejected() throws Exception
{
InvalidTypeIdException e = assertThrows(InvalidTypeIdException.class,
() -> blockingMapper().readValue(msgJson(ENV + "<" + GADGET + ">",
"\"payload\":{\"cmd\":\"x\"}"), MsgWrapper.class));
verifyException(e, "denied resolution of contained type");
verifyException(e, GADGET);
assertEquals(Collections.emptyList(), INSTANTIATIONS);
}
@Test
public void nestedGadgetAsTypeParameterRejected() throws Exception
{
InvalidTypeIdException e = assertThrows(InvalidTypeIdException.class,
() -> blockingMapper().readValue(
msgJson(ENV + "<java.util.ArrayList<" + GADGET + ">>",
"\"payload\":[{},{},{}]"), MsgWrapper.class));
verifyException(e, "denied resolution of contained type");
assertEquals(Collections.emptyList(), INSTANTIATIONS);
}
@Test
public void safeTypeParametersAccepted() throws Exception
{
ObjectMapper mapper = blockingMapper();
MsgWrapper w = mapper.readValue(msgJson(ENV, "\"payload\":\"a\""), MsgWrapper.class);
assertEquals("a", ((Env<?>) w.msg).payload);
w = mapper.readValue(msgJson(ENV + "<java.lang.String>", "\"payload\":\"b\""),
MsgWrapper.class);
assertEquals("b", ((Env<?>) w.msg).payload);
w = mapper.readValue(msgJson(ENV + "<java.lang.Integer>", "\"payload\":3"),
MsgWrapper.class);
assertEquals(Integer.valueOf(3), ((Env<?>) w.msg).payload);
w = mapper.readValue(msgJson(ENV + "<" + Color.class.getName() + ">",
"\"payload\":\"GREEN\""), MsgWrapper.class);
assertEquals(Color.GREEN, ((Env<?>) w.msg).payload);
w = mapper.readValue(msgJson(ENV + "<java.lang.Object>", "\"payload\":\"c\""),
MsgWrapper.class);
assertEquals("c", ((Env<?>) w.msg).payload);
w = mapper.readValue(msgJson(ENV + "<java.math.BigDecimal>", "\"payload\":1.25"),
MsgWrapper.class);
assertEquals(new java.math.BigDecimal("1.25"), ((Env<?>) w.msg).payload);
w = mapper.readValue(msgJson(ENV + "<java.math.BigInteger>", "\"payload\":7"),
MsgWrapper.class);
assertEquals(java.math.BigInteger.valueOf(7), ((Env<?>) w.msg).payload);
final UUID uuid = UUID.randomUUID();
w = mapper.readValue(msgJson(ENV + "<java.util.UUID>", "\"payload\":\"" + uuid + "\""),
MsgWrapper.class);
assertEquals(uuid, ((Env<?>) w.msg).payload);
}
/*
/**********************************************************************
/* Approval of outer base type must not approve type parameters
/**********************************************************************
*/
@Test
public void allowedBaseTypeDoesNotApproveTypeParameters() throws Exception
{
ObjectMapper mapper = jsonMapperBuilder()
.polymorphicTypeValidator(BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Msg.class)
.build())
.build();
// Approved base type: direct subtypes and safe type parameters fine
MsgWrapper w = mapper.readValue(msgJson(PlainMsg.class.getName(), "\"text\":\"x\""),
MsgWrapper.class);
assertEquals(PlainMsg.class, w.msg.getClass());
w = mapper.readValue(msgJson(ENV + "<java.lang.String>", "\"payload\":\"b\""),
MsgWrapper.class);
assertEquals("b", ((Env<?>) w.msg).payload);
InvalidTypeIdException e = assertThrows(InvalidTypeIdException.class,
() -> mapper.readValue(msgJson(ENV + "<" + GADGET + ">",
"\"payload\":{\"cmd\":\"x\"}"), MsgWrapper.class));
verifyException(e, "denied resolution of contained type");
// Should name the configured validator, not the internal wrapper
verifyException(e, BasicPolymorphicTypeValidator.class.getName());
assertEquals(Collections.emptyList(), INSTANTIATIONS);
e = assertThrows(InvalidTypeIdException.class,
() -> mapper.readValue(msgJson(ENV + "<" + SafeBean.class.getName() + ">",
"\"payload\":{\"data\":\"x\"}"), MsgWrapper.class));
verifyException(e, SafeBean.class.getName());
}
// Type parameter that is subtype of polymorphic base type: same checks as direct type id
@Test
public void baseSubtypeAsTypeParameterAccepted() throws Exception
{
final String plainMsgId = ENV + "<" + PlainMsg.class.getName() + ">";
// (payload is a `Msg` so has type id of its own)
final String json = msgJson(plainMsgId, "\"payload\":{\"@class\":\""
+ PlainMsg.class.getName() + "\",\"text\":\"x\"}");
ObjectMapper mapper = jsonMapperBuilder()
.polymorphicTypeValidator(BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Msg.class)
.build())
.build();
MsgWrapper w = mapper.readValue(json, MsgWrapper.class);
assertEquals("x", ((PlainMsg) ((Env<?>) w.msg).payload).text);
w = blockingMapper().readValue(json, MsgWrapper.class);
assertEquals("x", ((PlainMsg) ((Env<?>) w.msg).payload).text);
}
@Test
public void allowListedTypeParameterAccepted() throws Exception
{
ObjectMapper mapper = jsonMapperBuilder()
.polymorphicTypeValidator(BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Msg.class)
.allowIfSubType(SafeBean.class)
.build())
.build();
MsgWrapper w = mapper.readValue(msgJson(ENV + "<" + SafeBean.class.getName() + ">",
"\"payload\":{\"data\":\"x\"}"), MsgWrapper.class);
assertEquals("x", ((SafeBean) ((Env<?>) w.msg).payload).data);
}
@Test
public void minimalClassTypeParameterRejected() throws Exception
{
ObjectMapper mapper = jsonMapperBuilder()
.polymorphicTypeValidator(BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Iterable.class)
.build())
.build();
// (JSON Arrays use wrapper-array type id inclusion)
MinimalClassWrapper w = mapper.readValue(a2q(
"{'values':['java.util.ArrayList<java.lang.String>',['a']]}"),
MinimalClassWrapper.class);
assertEquals(Arrays.asList("a"), w.values);
InvalidTypeIdException e = assertThrows(InvalidTypeIdException.class,
() -> mapper.readValue(a2q(
"{'values':['java.util.ArrayList<" + GADGET + ">',[{}]]}"),
MinimalClassWrapper.class));
verifyException(e, "denied resolution of contained type");
assertEquals(Collections.emptyList(), INSTANTIATIONS);
}
/*
/**********************************************************************
/* Default typing with outer-base rule, Map key
/**********************************************************************
*/
private ObjectMapper mapDefaultTypingMapper() {
PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Map.class)
.build();
return jsonMapperBuilder()
.activateDefaultTyping(ptv, DefaultTyping.NON_FINAL)
.build();
}
@Test
public void defaultTypingMapKeyGadgetRejected() throws Exception
{
ObjectMapper mapper = mapDefaultTypingMapper();
InvalidTypeIdException e = assertThrows(InvalidTypeIdException.class,
() -> mapper.readValue(a2q("{'map':['java.util.HashMap<" + GADGET
+ ",java.lang.String>',{'attacker-chosen':'x'}]}"),
StringMapWrapper.class));
verifyException(e, "denied resolution of contained type");
verifyException(e, GADGET);
assertEquals(Collections.emptyList(), INSTANTIATIONS);
}
@Test
public void defaultTypingCompatibleMapAccepted() throws Exception
{
ObjectMapper mapper = mapDefaultTypingMapper();
StringMapWrapper input = new StringMapWrapper();
input.map = new HashMap<>();
input.map.put("a", "b");
String json = mapper.writeValueAsString(input);
assertEquals(input.map, mapper.readValue(json, StringMapWrapper.class).map);
StringMapWrapper w = mapper.readValue(a2q(
"{'map':['java.util.HashMap<java.lang.String,java.lang.String>',{'c':'d'}]}"),
StringMapWrapper.class);
assertEquals(Collections.singletonMap("c", "d"), w.map);
}
// Arrays are not gadgets by themselves: only their element type is validated
@Test
public void defaultTypingArrayTypeParameters() throws Exception
{
ObjectMapper mapper = mapDefaultTypingMapper();
ArrayMapWrapper w = mapper.readValue(a2q(
"{'map':['java.util.HashMap<java.lang.String,[Ljava.lang.String;>',{'k':['a','b']}]}"),
ArrayMapWrapper.class);
assertArrayEquals(new String[] { "a", "b" }, (String[]) w.map.get("k"));
w = mapper.readValue(a2q("{'map':['java.util.HashMap<java.lang.String,[I>',{'k':[1,2]}]}"),
ArrayMapWrapper.class);
assertArrayEquals(new int[] { 1, 2 }, (int[]) w.map.get("k"));
w = mapper.readValue(a2q("{'map':['java.util.HashMap<java.lang.String,[L"
+ Color.class.getName() + ";>',{'k':['RED']}]}"),
ArrayMapWrapper.class);
assertArrayEquals(new Color[] { Color.RED }, (Color[]) w.map.get("k"));
InvalidTypeIdException e = assertThrows(InvalidTypeIdException.class,
() -> mapper.readValue(a2q("{'map':['java.util.HashMap<java.lang.String,[L"
+ GADGET + ";>',{'k':[{}]}]}"),
ArrayMapWrapper.class));
verifyException(e, "denied resolution of contained type");
verifyException(e, GADGET);
assertEquals(Collections.emptyList(), INSTANTIATIONS);
}
/*
/**********************************************************************
/* Array type ids: element type must be validated too
/**********************************************************************
*/
private ObjectMapper serializableMapper(Class<?>... allowedSubTypes) {
BasicPolymorphicTypeValidator.Builder b = BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(java.io.Serializable.class);
for (Class<?> cls : allowedSubTypes) {
b = b.allowIfSubType(cls);
}
return jsonMapperBuilder()
.polymorphicTypeValidator(b.build())
.build();
}
@Test
public void arrayTypeIdGadgetElementRejected() throws Exception
{
// Every array type is `Serializable` (even if element type is not)
ObjectMapper mapper = serializableMapper();
InvalidTypeIdException e = assertThrows(InvalidTypeIdException.class,
() -> mapper.readValue(a2q("{'value':['[L" + GADGET + ";',[{'cmd':'x'}]]}"),
SerializableWrapper.class));
verifyException(e, "denied resolution of contained type");
verifyException(e, GADGET);
e = assertThrows(InvalidTypeIdException.class,
() -> mapper.readValue(a2q("{'value':['[[L" + GADGET + ";',[[{'cmd':'x'}]]]}"),
SerializableWrapper.class));
verifyException(e, "denied resolution of contained type");
assertEquals(Collections.emptyList(), INSTANTIATIONS);
e = assertThrows(InvalidTypeIdException.class,
() -> mapper.readValue(a2q("{'value':['[L" + SafeBean.class.getName()
+ ";',[{'data':'x'}]]}"), SerializableWrapper.class));
verifyException(e, SafeBean.class.getName());
}
@Test
public void arrayTypeIdSafeElementAccepted() throws Exception
{
SerializableWrapper w = serializableMapper().readValue(
a2q("{'value':['[Ljava.lang.String;',['a']]}"), SerializableWrapper.class);
assertArrayEquals(new String[] { "a" }, (String[]) w.value);
w = serializableMapper().readValue(a2q("{'value':['[I',[1]]}"), SerializableWrapper.class);
assertArrayEquals(new int[] { 1 }, (int[]) w.value);
w = serializableMapper(SafeBean.class).readValue(a2q("{'value':['[L"
+ SafeBean.class.getName() + ";',[{'data':'x'}]]}"), SerializableWrapper.class);
assertEquals("x", ((SafeBean[]) w.value)[0].data);
}
// Array type id for exactly the declared type: element type not selected by attacker
@Test
public void defaultTypingDeclaredArrayTypeAccepted() throws Exception
{
ObjectMapper mapper = jsonMapperBuilder()
.activateDefaultTyping(BasicPolymorphicTypeValidator.builder()
// approves array base type (but not `SafeBean` for base `Object`)
.allowIfBaseType(SafeBean[].class)
.allowIfBaseType(SafeBean.class)
.build(), DefaultTyping.NON_FINAL)
.build();
BeanArrayWrapper input = new BeanArrayWrapper();
SafeBean bean = new SafeBean();
bean.data = "x";
input.beans = new SafeBean[] { bean };
String json = mapper.writeValueAsString(input);
assertTrue(json.contains("[L" + SafeBean.class.getName() + ";"), json);
BeanArrayWrapper result = mapper.readValue(json, BeanArrayWrapper.class);
assertEquals("x", result.beans[0].data);
}
// Approval of declared array type covers arrays of subtypes of its element type
@Test
public void defaultTypingApprovedArrayBaseCoversElementSubtypes() throws Exception
{
ObjectMapper mapper = jsonMapperBuilder()
.activateDefaultTyping(BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(SafeBean[].class)
.build(), DefaultTyping.NON_FINAL)
.build();
BeanArrayWrapper result = mapper.readValue(a2q("{'beans':['[L"
+ FinalSubBean.class.getName() + ";',[{'extra':4}]]}"), BeanArrayWrapper.class);
assertEquals(FinalSubBean[].class, result.beans.getClass());
assertEquals(4, ((FinalSubBean) result.beans[0]).extra);
}
// Element type of array type id that is subtype of declared element type
@Test
public void defaultTypingDeclaredArraySubtypeAccepted() throws Exception
{
ObjectMapper mapper = jsonMapperBuilder()
.activateDefaultTyping(BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(SafeBean[].class)
.allowIfBaseType(SafeBean.class)
.build(), DefaultTyping.NON_FINAL)
.build();
BeanArrayWrapper input = new BeanArrayWrapper();
SubBean bean = new SubBean();
bean.data = "x";
bean.extra = 3;
input.beans = new SubBean[] { bean };
String json = mapper.writeValueAsString(input);
assertTrue(json.contains("[L" + SubBean.class.getName() + ";"), json);
BeanArrayWrapper result = mapper.readValue(json, BeanArrayWrapper.class);
assertEquals(SubBean[].class, result.beans.getClass());
assertEquals(3, ((SubBean) result.beans[0]).extra);
}
// ... but approval of declared array type does not cover any element type
@Test
public void defaultTypingDeclaredObjectArrayGadgetRejected() throws Exception
{
ObjectMapper mapper = jsonMapperBuilder()
.activateDefaultTyping(BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Object[].class)
.build(), DefaultTyping.NON_FINAL)
.build();
InvalidTypeIdException e = assertThrows(InvalidTypeIdException.class,
() -> mapper.readValue(a2q("{'values':['[L" + GADGET + ";',[]]}"),
ObjectArrayWrapper.class));
verifyException(e, "denied resolution of contained type");
verifyException(e, GADGET);
}
@Test
public void defaultTypingEnumContainersRoundTrip() throws Exception
{
ObjectMapper mapper = jsonMapperBuilder()
.activateDefaultTyping(BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Map.class)
.allowIfBaseType(Collection.class)
.build(), DefaultTyping.NON_FINAL)
.build();
EnumContainersWrapper input = new EnumContainersWrapper();
input.enumMap = new EnumMap<>(Color.class);
input.enumMap.put(Color.RED, "r");
input.enumSet = EnumSet.of(Color.GREEN);
String json = mapper.writeValueAsString(input);
// Jackson itself writes generic type ids for these:
assertTrue(json.contains("java.util.EnumMap<"), json);
assertTrue(json.contains("java.util.EnumSet<"), json);
EnumContainersWrapper result = mapper.readValue(json, EnumContainersWrapper.class);
assertEquals(input.enumMap, result.enumMap);
assertEquals(input.enumSet, result.enumSet);
}
/*
/**********************************************************************
/* Deny list applies to type parameters even if validator allows all
/**********************************************************************
*/
@Test
public void denyListedTypeParameterRejected() throws Exception
{
// Validator that allows everything for base type `Object`: contained types
// are allowed by validator, but not by deny list
ObjectMapper mapper = jsonMapperBuilder()
.polymorphicTypeValidator(BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Object.class)
.build())
.build();
InvalidDefinitionException e = assertThrows(InvalidDefinitionException.class,
() -> mapper.readValue(a2q("{'value':{'@class':'" + ENV
+ "<java.util.logging.FileHandler>','payload':{}}}"),
ObjectValueWrapper.class));
verifyException(e, "Illegal type");
verifyException(e, "prevented for security reasons");
verifyException(e, "java.util.logging.FileHandler");
}
/*
/**********************************************************************
/* Default validator
/**********************************************************************
*/
// Default validator allows subtypes of (safe) base type, but contained types
// need to be subtypes of base type as well (not just of `Object`)
@Test
public void defaultValidatorNonSubtypeTypeParameterRejected() throws Exception
{
ObjectMapper mapper = newJsonMapper();
InvalidTypeIdException e = assertThrows(InvalidTypeIdException.class,
() -> mapper.readValue(msgJson(ENV + "<" + GADGET + ">",
"\"payload\":{\"cmd\":\"x\"}"), MsgWrapper.class));
verifyException(e, "denied resolution of contained type");
verifyException(e, GADGET);
assertEquals(Collections.emptyList(), INSTANTIATIONS);
// also as element type of (nested) arrays
e = assertThrows(InvalidTypeIdException.class,
() -> mapper.readValue(msgJson(ENV + "<[[L" + GADGET + ";>",
"\"payload\":[[{\"cmd\":\"x\"}]]"), MsgWrapper.class));
verifyException(e, "denied resolution of contained type");
verifyException(e, GADGET);
assertEquals(Collections.emptyList(), INSTANTIATIONS);
MsgWrapper w = mapper.readValue(msgJson(ENV + "<java.lang.String>", "\"payload\":\"b\""),
MsgWrapper.class);
assertEquals("b", ((Env<?>) w.msg).payload);
w = mapper.readValue(msgJson(ENV + "<" + PlainMsg.class.getName() + ">",
"\"payload\":{\"@class\":\"" + PlainMsg.class.getName() + "\",\"text\":\"x\"}"),
MsgWrapper.class);
assertEquals("x", ((PlainMsg) ((Env<?>) w.msg).payload).text);
}
/*
/**********************************************************************
/* Array type ids approved by sub-type matcher
/**********************************************************************
*/
@Test
public void arrayTypeIdApprovedBySubTypeMatcher() throws Exception
{
// Every array type is `Serializable`, even if element type is not
ObjectMapper mapper = jsonMapperBuilder()
.polymorphicTypeValidator(BasicPolymorphicTypeValidator.builder()
.allowIfSubType(java.io.Serializable.class)
.build())
.build();
InvalidTypeIdException e = assertThrows(InvalidTypeIdException.class,
() -> mapper.readValue(a2q("{'value':['[L" + GADGET + ";',[{'cmd':'x'}]]}"),
ObjectValueWrapper.class));
verifyException(e, "denied resolution of contained type");
verifyException(e, GADGET);
assertEquals(Collections.emptyList(), INSTANTIATIONS);
ObjectValueWrapper w = mapper.readValue(a2q("{'value':['[Ljava.lang.String;',['a']]}"),
ObjectValueWrapper.class);
assertArrayEquals(new String[] { "a" }, (String[]) w.value);
}
// Abstract element types are not exempt: may still be deserializable
@Test
public void abstractArrayElementTypeValidated() throws Exception
{
// Name-only allow-list for the generic type
ObjectMapper mapper = jsonMapperBuilder()
.polymorphicTypeValidator(BasicPolymorphicTypeValidator.builder()
.allowIfSubType(ENV)
.build())
.build();
InvalidTypeIdException e = assertThrows(InvalidTypeIdException.class,
() -> mapper.readValue(msgJson(ENV + "<[L" + ABSTRACT_GADGET + ";>",
"\"payload\":[{\"cmd\":\"x\"}]"), MsgWrapper.class));
verifyException(e, "denied resolution of contained type");
verifyException(e, ABSTRACT_GADGET);
assertEquals(Collections.emptyList(), INSTANTIATIONS);
// Array type id approved by sub-type matcher (every array is `Serializable`)
ObjectMapper mapper2 = jsonMapperBuilder()
.polymorphicTypeValidator(BasicPolymorphicTypeValidator.builder()
.allowIfSubType(java.io.Serializable.class)
.build())
.build();
e = assertThrows(InvalidTypeIdException.class,
() -> mapper2.readValue(a2q("{'value':['[L" + ABSTRACT_GADGET + ";',[{'cmd':'x'}]]}"),
ObjectValueWrapper.class));
verifyException(e, "denied resolution of contained type");
verifyException(e, ABSTRACT_GADGET);
assertEquals(Collections.emptyList(), INSTANTIATIONS);
}
// ... even with `allowIfSubTypeIsArray()`
@Test
public void abstractArrayElementTypeWithArraySupport() throws Exception
{
ObjectMapper mapper = jsonMapperBuilder()
.polymorphicTypeValidator(BasicPolymorphicTypeValidator.builder()
.allowIfSubType(ENV)
.allowIfSubTypeIsArray()
.build())
.build();
InvalidTypeIdException e = assertThrows(InvalidTypeIdException.class,
() -> mapper.readValue(a2q("{'value':['[L" + ABSTRACT_GADGET + ";',[{'cmd':'x'}]]}"),
ObjectValueWrapper.class));
verifyException(e, ABSTRACT_GADGET);
e = assertThrows(InvalidTypeIdException.class,
() -> mapper.readValue(msgJson(ENV + "<[L" + ABSTRACT_GADGET + ";>",
"\"payload\":[{\"cmd\":\"x\"}]"), MsgWrapper.class));
verifyException(e, ABSTRACT_GADGET);
assertEquals(Collections.emptyList(), INSTANTIATIONS);
// but fine if allow-listed
ObjectMapper mapper2 = jsonMapperBuilder()
.polymorphicTypeValidator(BasicPolymorphicTypeValidator.builder()
.allowIfSubType(Msg.class)
.allowIfSubTypeIsArray()
.build())
.build();
MsgWrapper w = mapper2.readValue(msgJson(ENV + "<[L" + Msg.class.getName() + ";>",
"\"payload\":[{\"@class\":\"" + ENV + "\"}]"), MsgWrapper.class);
Object[] payload = (Object[]) ((Env<?>) w.msg).payload;
assertEquals(Msg[].class, payload.getClass());
assertEquals(Env.class, payload[0].getClass());
}
/**
* Stand-in gadget type that is `Serializable` (and `Cloneable`), like many real
* ones; final so that `NON_FINAL` default typing gives its instances no type id
* of their own, which would be validated separately
*/
public static final class SerializableGadget extends Gadget implements java.io.Serializable, Cloneable {
private static final long serialVersionUID = 1L;
}
// (final: no type id on the wrapper itself under `NON_FINAL` default typing)
static final class SerializableMatrixWrapper {
public java.io.Serializable[][] value;
}
static final class ObjectMatrixWrapper {
public Object[][] value;
}
static final class ObjectFieldWrapper {
public Object value;
}
/**
* Approves any array (and {@code Env}) for any base type, and allows arrays
* explicitly (so that their element types are not validated) for base
* {@code Object[][]} only
*/
static class ExplicitForOneBaseTypeValidator extends PolymorphicTypeValidator.Base {
private static final long serialVersionUID = 1L;
@Override
public Validity validateSubType(DatabindContext ctxt, JavaType baseType,
JavaType subType) {
// (and `Env`, so that an array type parameter of it gets looked at)
return (subType.isArrayType() || subType.hasRawClass(Env.class))
? Validity.ALLOWED : Validity.INDETERMINATE;
}
@Override
public boolean isArrayTypeAllowedExplicitly(DatabindContext ctxt, JavaType baseType,
JavaType arrayType) {
return baseType.hasRawClass(Object[][].class);
}
}
private static String nestedArrayJson(String property, String elementType, int depth)
{
String dims = "", open = "", close = "";
for (int i = 0; i < depth; ++i) {
dims += "["; open += "["; close += "]";
}
return a2q("{'" + property + "':['" + dims + "L" + elementType + ";',"
+ open + "{'cmd':'x'}" + close + "]}");
}
private void _assertGadgetArrayRejected(ObjectMapper mapper, String json, Class<?> target,
String rule)
{
InvalidTypeIdException e = assertThrows(InvalidTypeIdException.class,
() -> mapper.readValue(json, target), rule);
verifyException(e, "denied resolution of contained type");
verifyException(e, SerializableGadget.class.getName());
assertEquals(Collections.emptyList(), INSTANTIATIONS);
}
// A rule every array of some depth matches (`Object[][]` for all 2-D
// reference arrays, `Serializable[]` for all arrays of `Serializable`
// elements, a name pattern for all non-JDK arrays) approves the array and
// not its element type, whatever the depth of the type id
@Test
public void arrayRuleMatchingEveryArrayOfDepthRejected() throws Exception
{
final String gadget = SerializableGadget.class.getName();
final Object[][] cases = {
// rule, depth of the type id
{ Object[][].class, 2 }, { Object[][][].class, 3 },
{ java.io.Serializable[].class, 1 }, { java.io.Serializable[][].class, 2 },
{ Cloneable[].class, 1 }, { Cloneable[][].class, 2 },
// rule for one depth, type id of a deeper one (`Gadget[][]` is an `Object[]`)
{ Object[].class, 2 }, { Object[][].class, 3 },
// name pattern matching all 2-D arrays of non-JDK classes
{ java.util.regex.Pattern.compile("\\[\\[L(?!java\\.).*"), 2 },
};
for (Object[] c : cases) {
BasicPolymorphicTypeValidator.Builder b = BasicPolymorphicTypeValidator.builder();
if (c[0] instanceof Class<?>) {
b = b.allowIfSubType((Class<?>) c[0]);
} else {
b = b.allowIfSubType((java.util.regex.Pattern) c[0]);
}
ObjectMapper mapper = jsonMapperBuilder()
.polymorphicTypeValidator(b.build())
.build();
_assertGadgetArrayRejected(mapper, nestedArrayJson("value", gadget, (Integer) c[1]),
ObjectValueWrapper.class, String.valueOf(c[0]));
}
// but a rule for exactly the 2-D array class covers its elements
ObjectMapper mapper = jsonMapperBuilder()
.polymorphicTypeValidator(BasicPolymorphicTypeValidator.builder()
.allowIfSubType(SafeBean[][].class)
.build())
.build();
ObjectValueWrapper w = mapper.readValue(a2q("{'value':['[[L" + SafeBean.class.getName()
+ ";',[[{'data':'x'}]]]}"), ObjectValueWrapper.class);
assertEquals("x", ((SafeBean[][]) w.value)[0][0].data);
}
// A name rule for JDK arrays matches every array of a JDK element type, so
// the element type is validated on its own (`Date` here: not exempt, not
// allow-listed, and harmless if ever constructed)
@Test
public void arrayRuleMatchingJdkArraysRejected() throws Exception
{
for (Object rule : new Object[] { "[Ljava.", java.util.regex.Pattern.compile("\\[+Ljava\\..*") }) {
BasicPolymorphicTypeValidator.Builder b = BasicPolymorphicTypeValidator.builder();
b = (rule instanceof String) ? b.allowIfSubType((String) rule)
: b.allowIfSubType((java.util.regex.Pattern) rule);
ObjectMapper mapper = jsonMapperBuilder()
.polymorphicTypeValidator(b.build())
.build();
InvalidTypeIdException e = assertThrows(InvalidTypeIdException.class,
() -> mapper.readValue(a2q("{'value':['[Ljava.util.Date;',[0]]}"),
ObjectValueWrapper.class), String.valueOf(rule));
verifyException(e, "denied resolution of contained type");
verifyException(e, "java.util.Date");
}
}
// Approval of a declared `Serializable[][]` (or `Object[][]`, `Cloneable[][]`)
// base type covers subtypes of its element type no more than a subtype rule
// for it does: every gadget is one
@Test
public void approvedUniversalArrayBaseTypeDoesNotCoverElements() throws Exception
{
ObjectMapper mapper = jsonMapperBuilder()
.activateDefaultTyping(BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(java.io.Serializable[][].class)
.build(), DefaultTyping.NON_FINAL)
.build();
_assertGadgetArrayRejected(mapper,
nestedArrayJson("value", SerializableGadget.class.getName(), 2),
SerializableMatrixWrapper.class, "allowIfBaseType(Serializable[][])");
}
// The validator is asked about an array type it approved as subtype, with the
// base type that was approved for; inner dimensions and array type parameters
// are never asked about, since no validator approved them
@Test
public void explicitSignalOnlyForApprovedArrayAndItsBaseType() throws Exception
{
final String gadget = SerializableGadget.class.getName();
ObjectMapper mapper = jsonMapperBuilder()
.activateDefaultTyping(new ExplicitForOneBaseTypeValidator(), DefaultTyping.NON_FINAL)
.build();
// base type `Object[][]`: allowed explicitly, elements not validated
ObjectMatrixWrapper w = mapper.readValue(nestedArrayJson("value", gadget, 2),
ObjectMatrixWrapper.class);
assertEquals(SerializableGadget.class, w.value[0][0].getClass());
INSTANTIATIONS.clear();
// base type `Object`: same array type, not allowed explicitly
_assertGadgetArrayRejected(mapper, nestedArrayJson("value", gadget, 2),
ObjectFieldWrapper.class, "base Object");
// as type parameter: not asked about at all
InvalidTypeIdException e = assertThrows(InvalidTypeIdException.class,
() -> mapper.readValue(a2q("{'value':['" + ENV + "<[[L" + gadget + ";>',"
+ "{'payload':[[{'cmd':'x'}]]}]}"), ObjectFieldWrapper.class));
verifyException(e, "denied resolution of contained type");
verifyException(e, gadget);
assertEquals(Collections.emptyList(), INSTANTIATIONS);
}
// Approval of an array type covers its element type only when given for
// exactly that class: `allowIfSubType(Bar[].class)` names the application's
// choice of elements, a name pattern matching `[Lcom.foo.Bar;` cannot be told
// from one matching every array of a package or depth. And only for an array
// named directly by a type id: as a type parameter, the array itself is not
// approved by any rule, so its element type needs one
@Test
public void explicitArrayRuleCoversElementType() throws Exception
{
final String json = a2q("{'value':['[L" + SafeBean.class.getName() + ";',[{'data':'x'}]]}");
final String asParameter = a2q("{'value':['" + ENV + "<[L" + SafeBean.class.getName()
+ ";>',{'payload':[{'data':'y'}]}]}");
ObjectMapper mapper = jsonMapperBuilder()
.polymorphicTypeValidator(BasicPolymorphicTypeValidator.builder()
.allowIfSubType(Env.class)
.allowIfSubType(SafeBean[].class)
.build())
.build();
ObjectValueWrapper w = mapper.readValue(json, ObjectValueWrapper.class);
assertEquals("x", ((SafeBean[]) w.value)[0].data);
InvalidTypeIdException e = assertThrows(InvalidTypeIdException.class,
() -> mapper.readValue(asParameter, ObjectValueWrapper.class));
verifyException(e, "denied resolution of contained type");
verifyException(e, SafeBean.class.getName());
ObjectMapper allowing = jsonMapperBuilder()
.polymorphicTypeValidator(BasicPolymorphicTypeValidator.builder()
.allowIfSubType(Env.class)
.allowIfSubType(SafeBean[].class)
.allowIfSubType(SafeBean.class)
.build())
.build();
w = allowing.readValue(asParameter, ObjectValueWrapper.class);
assertEquals("y", ((SafeBean[]) ((Env<?>) w.value).payload)[0].data);
// but not other array types
e = assertThrows(InvalidTypeIdException.class,
() -> mapper.readValue(a2q("{'value':['[L" + GADGET + ";',[{'cmd':'x'}]]}"),
ObjectValueWrapper.class));
verifyException(e, GADGET);
assertEquals(Collections.emptyList(), INSTANTIATIONS);
// nor deeper arrays of the same element type: `SafeBean[][]` approved by a
// rule every 2-D array matches, `SafeBean` itself still validated
ObjectMapper deeper = jsonMapperBuilder()
.polymorphicTypeValidator(BasicPolymorphicTypeValidator.builder()
.allowIfSubType(SafeBean[].class)
.allowIfSubType(Object[].class)
.build())
.build();
e = assertThrows(InvalidTypeIdException.class,
() -> deeper.readValue(a2q("{'value':['[[L" + SafeBean.class.getName()
+ ";',[[{'data':'x'}]]]}"), ObjectValueWrapper.class));
verifyException(e, "denied resolution of contained type");
verifyException(e, SafeBean.class.getName());
// arrays of subtypes of the listed element type are covered, as by any class rule
ObjectValueWrapper sub = mapper.readValue(a2q("{'value':['[L" + FinalSubBean.class.getName()
+ ";',[{'data':'x','extra':4}]]}"), ObjectValueWrapper.class);
assertEquals(4, ((FinalSubBean[]) sub.value)[0].extra);
}
// The array class named by the rule counts whatever its element type: elements
// of an exactly named `Serializable[]` are of an abstract type and get type ids
// of their own
@Test
public void exactUniversalArrayRuleCoversItself() throws Exception
{
ObjectMapper mapper = jsonMapperBuilder()
.polymorphicTypeValidator(BasicPolymorphicTypeValidator.builder()
.allowIfSubType(java.io.Serializable[].class)
.build())
.build();
ObjectValueWrapper w = mapper.readValue(a2q("{'value':['[Ljava.io.Serializable;',[]]}"),
ObjectValueWrapper.class);
assertEquals(java.io.Serializable[].class, w.value.getClass());
// but not an array of a `Serializable` gadget
_assertGadgetArrayRejected(mapper, nestedArrayJson("value", SerializableGadget.class.getName(), 1),
ObjectValueWrapper.class, "allowIfSubType(Serializable[])");
}
@Test
public void nameBasedArrayRuleDoesNotCoverElementType() throws Exception
{
final String json = a2q("{'value':['[L" + SafeBean.class.getName() + ";',[{'data':'x'}]]}");
final java.util.regex.Pattern exactName = java.util.regex.Pattern.compile(
java.util.regex.Pattern.quote("[L" + SafeBean.class.getName() + ";"));
ObjectMapper mapper = jsonMapperBuilder()
.polymorphicTypeValidator(BasicPolymorphicTypeValidator.builder()
.allowIfSubType(exactName)
.build())
.build();
InvalidTypeIdException e = assertThrows(InvalidTypeIdException.class,
() -> mapper.readValue(json, ObjectValueWrapper.class));
verifyException(e, "denied resolution of contained type");
verifyException(e, SafeBean.class.getName());
// with a rule for the element type as well: fine
ObjectMapper allowing = jsonMapperBuilder()
.polymorphicTypeValidator(BasicPolymorphicTypeValidator.builder()
.allowIfSubType(exactName)
.allowIfSubType(SafeBean.class)
.build())
.build();
assertEquals("x", ((SafeBean[]) allowing.readValue(json, ObjectValueWrapper.class).value)[0].data);
}
// ... but not approval that covers all array types (like `Object[]`)
@Test
public void nonSpecificallyApprovedArrayTypeElementValidated() throws Exception
{
ObjectMapper mapper = jsonMapperBuilder()
.polymorphicTypeValidator(BasicPolymorphicTypeValidator.builder()
.allowIfSubType("[L")
.build())
.build();
InvalidTypeIdException e = assertThrows(InvalidTypeIdException.class,
() -> mapper.readValue(a2q("{'value':['[L" + GADGET + ";',[{'cmd':'x'}]]}"),
ObjectValueWrapper.class));
verifyException(e, "denied resolution of contained type");
verifyException(e, GADGET);
assertEquals(Collections.emptyList(), INSTANTIATIONS);
}
/*
/**********************************************************************
/* JDK containers as type parameters
/**********************************************************************
*/
@Test
public void jdkContainerTypeParametersAccepted() throws Exception
{
ObjectMapper mapper = jsonMapperBuilder()
.polymorphicTypeValidator(BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Msg.class)
.build())
.build();
MsgWrapper w = mapper.readValue(msgJson(ENV + "<java.util.List<java.lang.String>>",
"\"payload\":[\"a\"]"), MsgWrapper.class);
assertEquals(Arrays.asList("a"), ((Env<?>) w.msg).payload);
w = mapper.readValue(msgJson(ENV + "<java.util.ArrayList<java.lang.String>>",
"\"payload\":[\"b\"]"), MsgWrapper.class);
assertEquals(Arrays.asList("b"), ((Env<?>) w.msg).payload);
w = mapper.readValue(msgJson(ENV + "<java.util.HashMap<java.lang.String,java.lang.Integer>>",
"\"payload\":{\"c\":1}"), MsgWrapper.class);
assertEquals(Collections.singletonMap("c", 1), ((Env<?>) w.msg).payload);
w = mapper.readValue(msgJson(ENV
+ "<java.util.concurrent.atomic.AtomicReference<java.lang.String>>",
"\"payload\":\"d\""), MsgWrapper.class);
assertEquals("d", ((java.util.concurrent.atomic.AtomicReference<?>) ((Env<?>) w.msg).payload).get());
// ... but their contents are still validated
InvalidTypeIdException e = assertThrows(InvalidTypeIdException.class,
() -> mapper.readValue(msgJson(ENV + "<java.util.ArrayList<" + GADGET + ">>",
"\"payload\":[{}]"), MsgWrapper.class));
verifyException(e, "denied resolution of contained type");
verifyException(e, GADGET);
assertEquals(Collections.emptyList(), INSTANTIATIONS);
}
/*
/**********************************************************************
/* Validators that deny `Object` as base type
/**********************************************************************
*/
// Intentional: contained types that are not subtypes of base type are validated
// as type ids for base type `Object`, which such validators deny
@Test
public void objectBaseDenyingValidatorRejectsAllowListedTypeParameter() throws Exception
{
ObjectMapper mapper = jsonMapperBuilder()
.polymorphicTypeValidator(BasicPolymorphicTypeValidator.builder()
.allowIfSubType(Msg.class)
.allowIfSubType(SafeBean.class)
.denyForExactBaseType(Object.class)
.build())
.build();
InvalidTypeIdException e = assertThrows(InvalidTypeIdException.class,
() -> mapper.readValue(msgJson(ENV + "<" + SafeBean.class.getName() + ">",
"\"payload\":{\"data\":\"x\"}"), MsgWrapper.class));
verifyException(e, "denied resolution of contained type");
verifyException(e, SafeBean.class.getName());
}
private String msgJson(String typeId, String props) {
return "{\"msg\":{\"@class\":\"" + typeId + "\"," + props + "}}";
}
}