Jackson 2.10: Safe Default Typing

aka stemming the flow of Jackson-databind CVEs

(note: this story is part of deeper dive of Jackson 2.10 release features, after Overview of Jackson 2.10)

Arguably the most important new/improved feature in Jackson 2.10 is so-called “Safe Default Typing”. It is the replacement of existing basic unbounded Default Typing, problems of which are explained in earlier “On Jackson CVEs: Don’t Panic!” blog post.

What is Safe Default Typing?

How is Safe Default Typing enabled?

ObjectMapper mapper = new ObjectMapper();
PolymorphicTypeValidator ptv = ...; // see examples below
mapper.activateDefaultTyping(DefaultTyping.NON_FINAL, ptv);

as well as new builder-style construction (added in 2.10, only mechanism in 3.0+):

ObjectMapper mapper = JsonMapper.builder()
.activateDefaultTyping(DefaultTyping.NON_FINAL, ptv)
.build();

Regardless of method, resulting ObjectMapper in this case would add class name as type id for all properties of type that is not final , similar to earlier Default Typing. The only difference is the use of explicit validator.

Note, too, that 2.10 deprecates unsafe variants named enableDefaultTyping() (and 3.0 will remove them for good).

Where do I get a PolymorphicTypeValidator

PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator
.builder()
.allowIfBaseType(MyThing.class)
.build();

which would allow polymorphic deserialization of all values of properties of type MyThing (or its subtypes).

Note that evaluation order for rules in same category (for base type, or for sub types) are evaluated in order added: first rule first, last rule last. This mostly matters if you want to apply “denyForExactBaseType()” rule, which would typically come before allow rules (it may be used, for example, to deny use of java.lang.Object as base type for any polymorphic values).

When and how is validation applied?

Validators are called during multiple different but related phases:

  1. When a Bean property definition is encountered for type that is to use Default Typing (typically when constructing a BeanDeserializer): validateBaseType() is called. This (like all methods called) may return one of Validity result values ( ALLOWED, DENIED or INDETERMINATE) or throw an exception (which effectively another way to indicate DENIED but allows customizing of error message)
  2. When an actual Type Id (class name) is first resolved into Class: validateSubClassName() is called
  3. Before locating a deserializer for resolved Class : validateSubType() is called

Of these, (1) is always called first, once per property definition: (2) is called once per distinct subclass for actual values if (1) does not return determinate result. If (2) does not return determinate result, (3) is called: and finally, if even that is indeterminate, resolution is denied (i.e. default empty implementation would NOT allow any subclasses) and a JsonMappingException is thrown with brief mention that subtype resolution was not allowed.

Example validation: basic inclusion

public final class MyPolyBean {
public MyValue value;
}
public abstract class MyValue {
}
// and some implementations like
public class MySubType extends MyValue {
public int x;
}

Assuming configuration using DefaultTyping.NON_FINAL, serializing an instance of MyPolyBean we would get JSON output like

{ "value" : [ "MySubType", { "x", 3 } ] }

(note that no Type Id is used for MyPolyBean as it is final)

When deserializing, Jackson would notice that declared base type of “value” is MyValue and would call method PolymorphicTypeValidator.validateBaseType(). There are 3 result values:

  1. Validity.ALLOWED : allow all values of property since base type itself is validated
  2. Validity.DENIED: do not allow deserialization of property at all (base type itself considered unsafe). Sometimes used to prevent use of fundamentally unsafe types such as java.lang.Object or java.io.Serializable
  3. Validity.INDETERMINATE: no conclusion can be drawn on base type, need to validate actual subclass id

Of these, the last choice is most common. Let’s now consider this validator:

PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator
.builder()
.allowIfBaseType(MyValue.class)
.build();

in this case, call to validateBaseType would return Validity.ALLOWED and deserialization of all legal values would proceed without further checks.

But we could have chosen a different legal validator as well:

PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator
.builder()
.allowIfSubType(MySubType.class)
.build();

in which case first check would return Validity.INDETERMINATE, and a further call would be made, first with class name “MySubType”, and since that is not defined for match, after resolving to MySubType.class, finally call that does match allowed class we indicate.

Note that there are also variations for both base and subtypes that allow use of class name: allowIfBaseType(Pattern) (name matches regular expression) /allowIfBaseType(String) (name has specified prefix, usually package name) and allowIfSubType(Pattern) / allowIfSubType(String):

PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator
.builder()
.allowIfBaseType("com.mycorporation.")
.build();

Example validation: more advanced

class Wrapper<T> {
public T value;
}

in which case base type would be T which (without further bounds) must be introspected as just java.lang.Object. We do NOT want to just allow allObject.class subtypes — that is literally anything, and the reason why there were potential attacks using Default Typing! — but need something bit more sophisticated.

One possibility is that values we have consist of a small number of disjoint types. For example, we could use:

PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator
.builder()
.allowIfSubType(Map.class)
.allowIfSubType(List.class)
.allowIfSubType("com.myworkplace.")
.allowIfSubType(Number.class)
.allowIfSubType(SpecialValue.class)
.build();

where “subtype” rules must be used as base type (Object) is too general to allow determination.

PolymorphicTypeValidator with “@JsonTypeInfo”

// if doing this, you better configure validator!
class ValueWrapper {
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
public Object value;
}
PolymorphicTypeValidator ptv = ....;
ObjectMapper mapper = JsonMapper.builder()
.polymorphicTypeValidator(ptv)
.build(); // with 2.10 and later (including 3.0)
// OR
mapper.setPolymorphicTypeValidator(ptv); // with 2.x only

Note that although you can use same validator instance, you will need to configure these separately.

Further considerations

But you can also quite easily implement your own validators with custom logic. For example:

public class MyValidator extends PolymorphicTypeValidator.Base
{
public Validity validateSubClassName((MapperConfig<?> config, JavaType baseType, String subClassName) {
return subClassName.startsWith("com.foobar.") ?
Validity.ALLOWED : Validity.INDETERMINATE;
}
}

would allow all sub-classes in package com.foobar (and its sub-packages).

Written by

Open Source developer, most known for Jackson data processor (nee “JSON library”), author of many, many other OSS libraries for Java, from ClassMate to Woodstox

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store