37 runnable examples covering the eight feature posts on ankurm.com, verified against Jackson 3.2.1 on Temurin 21.0.5. Every output committed under docs/ was produced by run-all.sh. Also documents 11 places where the published snippets do not compile or do not behave as printed against a real Jackson 3 build - most notably that writeValueAsString(List<Base>) silently drops the polymorphic type discriminator, so the post's serialised output cannot be read back.
4.6 KiB
Part 7 — Security
Post: https://ankurm.com/jackson-security-best-practices/
H01 — the safe pattern
H01SafePolymorphismByAnnotation.java
@JsonTypeInfo(use = Id.NAME) plus an explicit @JsonSubTypes registry. The permitted
set is fixed at compile time, so no payload can introduce a class of its own.
email : EmailNotification[a@example.com]
sms : SmsNotification[+441234567890]
attack: rejected with InvalidTypeIdException
This is the recommendation, and it is correct. Reach for anything below only if this cannot express your model.
H02 — what survived into Jackson 3
The post's remediation snippet is Jackson 2:
ObjectMapper mapper = new ObjectMapper();
mapper.activateDefaultTyping(safeTypeValidator, ObjectMapper.DefaultTyping.NON_FINAL, ...);
Neither line compiles against Jackson 3. The mapper has no mutators at all, and
DefaultTyping is no longer nested inside ObjectMapper.
--- tools.jackson.databind.ObjectMapper ---
enableDefaultTyping ABSENT
activateDefaultTyping ABSENT
setSerializationInclusion ABSENT
registerModule ABSENT
addMixIn ABSENT
total set*() mutators: 0
--- tools.jackson.databind.json.JsonMapper.Builder ---
activateDefaultTyping present
deactivateDefaultTyping present
polymorphicTypeValidator present
changeDefaultPropertyInclusion present
serializationInclusion ABSENT
A second correction, from the migration repo rather than this one: both guides state
that enableDefaultTyping() was "removed in 2.16". It is still present on
ObjectMapper in Jackson 2.22.1, deprecated. That changes the migration story — a
Jackson 2 codebase can still be compiling against it today, so Jackson 3 is where it
finally breaks, not 2.16.
H03 — a provable allowlist
H03PolymorphicTypeValidatorAllowlist.java
The post's intent in working Jackson 3 form, with the negative test the post's own AI prompt asks for.
allowed written : {"@class":"com.ankurm.jackson3.part7security.H03PolymorphicTypeValidatorAllowlist$Envelope","body":{"@class":"com.ankurm.jackson3.part7security.H03PolymorphicTypeValidatorAllowlist$SafePayload","note":"ok"}}
allowed read : SafePayload[ok]
rogue : rejected with InvalidTypeIdException
One trap the post does not mention: with DefaultTyping.NON_FINAL, Jackson writes a
type id for the root object too, so the root class must be allowlisted as well.
Allowlisting only the payload base type makes the happy path fail, not just the attack
path — which reads like a bug in your validator when it is actually correct behaviour.
H04 — resource limits
Beyond the post. Gadget attacks are not the only deserialisation risk: a few hundred bytes of nested brackets can exhaust the stack. Jackson 3 ships defaults for this and they are tunable.
--- Jackson 3 defaults ---
max nesting depth : 500
max number length : 1000
max string length : 100000000
max name length : 50000
max doc length : -1 (-1 = unlimited)
1200-deep nesting, default limits -> rejected (StreamConstraintsException)
20-deep nesting, strict limits -> rejected (StreamConstraintsException)
5-deep nesting, strict limits -> accepted
3KB string, strict limits -> rejected (StreamConstraintsException)
The 100 MB default string limit and unlimited document length are generous for a public endpoint. Tighten both if you accept untrusted JSON.
H05 — Object.class
H05NeverDeserialiseIntoObject.java
as Object : {action=ship, quantity=3, extra={nested=[1, 2]}}
runtime type: java.util.LinkedHashMap <- a plain Map, no arbitrary class was instantiated
as DTO : MyRequestDto[action=ship, quantity=3]
bad input : rejected with InvalidFormatException
Worth being precise here, because the post is slightly alarming about it: on a default
Jackson 3 mapper, readValue(json, Object.class) yields a plain LinkedHashMap. No
arbitrary class is instantiated. The danger returns only once default typing is active
(H03). The rule still holds — target a specific type and you get input validation as a
side effect, as the last line shows.