1
0

Part 7: PolymorphicTypeValidator in working Jackson 3 form, with a negative test

This commit is contained in:
2026-08-04 17:34:27 +00:00
parent af38825c7a
commit 3d4ec4cfbc

View File

@@ -0,0 +1,75 @@
package com.ankurm.jackson3.part7security;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import tools.jackson.databind.DefaultTyping;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
import tools.jackson.databind.jsontype.PolymorphicTypeValidator;
/**
* Post: Jackson Security Best Practices — https://ankurm.com/jackson-security-best-practices/
* Section: "The Safe Alternative: PolymorphicTypeValidator"
*
* The post's snippet in working Jackson 3 form. Note DefaultTyping is a top-level
* enum in tools.jackson.databind, not ObjectMapper.DefaultTyping as in Jackson 2.
*
* Default typing remains a last resort. Prefer H01. This exists because legacy object
* graphs and plugin systems sometimes genuinely need it, and when they do, the
* allowlist has to be provable — hence the negative test at the bottom.
*/
public class H03PolymorphicTypeValidatorAllowlist {
public abstract static class BasePayload { }
public static class SafePayload extends BasePayload {
public String note;
public SafePayload() { }
public SafePayload(String n) { note = n; }
@Override public String toString() { return "SafePayload[" + note + "]"; }
}
/** Deliberately outside the allowlisted base type. */
public static class RoguePayload {
public String note;
}
public static class Envelope {
public Object body; // the field default typing has to resolve
public Envelope() { }
public Envelope(Object b) { body = b; }
}
public static void main(String[] args) {
// Permit ONLY the envelope and our own payload hierarchy. Anything else is
// refused at type-resolution time, before any class is instantiated.
//
// Note the gotcha: with DefaultTyping.NON_FINAL, Jackson writes a type id for
// the ROOT object too, so Envelope must be allowlisted as well. Allowlisting
// only BasePayload makes even the happy path fail — which is how most people
// first meet this API.
PolymorphicTypeValidator safeTypeValidator = BasicPolymorphicTypeValidator.builder()
.allowIfSubType(Envelope.class)
.allowIfSubType(BasePayload.class)
.build();
JsonMapper mapper = JsonMapper.builder()
.activateDefaultTyping(safeTypeValidator,
DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY)
.build();
String allowed = mapper.writeValueAsString(new Envelope(new SafePayload("ok")));
System.out.println("allowed written : " + allowed);
System.out.println("allowed read : "
+ ((Envelope) mapper.readValue(allowed, Envelope.class)).body);
// Negative test: a class outside the allowlist is rejected even though it
// exists on the classpath and would deserialise perfectly well otherwise.
String rogue = "{\"body\":[\"" + RoguePayload.class.getName() + "\",{\"note\":\"pwn\"}]}";
try {
mapper.readValue(rogue, Envelope.class);
System.out.println("rogue : UNEXPECTEDLY ACCEPTED");
} catch (Exception e) {
System.out.println("rogue : rejected with " + e.getClass().getSimpleName());
}
}
}