1
0
Files
jackson3-by-example/docs/part7-security.md
Ankur c438afc33b Jackson 3 series companion code
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.
2026-08-04 23:29:26 +05:30

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

H02DefaultTypingRemoved.java

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

H04StreamReadConstraints.java

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.