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.
This commit is contained in:
116
docs/part7-security.md
Normal file
116
docs/part7-security.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user