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.
117 lines
4.6 KiB
Markdown
117 lines
4.6 KiB
Markdown
# Part 7 — Security
|
|
|
|
Post: <https://ankurm.com/jackson-security-best-practices/>
|
|
|
|
## H01 — the safe pattern
|
|
|
|
[`H01SafePolymorphismByAnnotation.java`](../src/main/java/com/ankurm/jackson3/part7security/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`](../src/main/java/com/ankurm/jackson3/part7security/H02DefaultTypingRemoved.java)
|
|
|
|
The post's remediation snippet is Jackson 2:
|
|
|
|
```java
|
|
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`](../src/main/java/com/ankurm/jackson3/part7security/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`](../src/main/java/com/ankurm/jackson3/part7security/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`](../src/main/java/com/ankurm/jackson3/part7security/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.
|