Part 7: PolymorphicTypeValidator in working Jackson 3 form, with a negative test
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user