A team configures their Kafka JSON deserializer with Jackson's polymorphic default typing so the message type is embedded in the payload. What is the risk, and how would you remediate it?
answer
- @class / @type embeds a Java class name
- activateDefaultTyping → gadget chain → RCE
- deny-list lost; allowlist with PolymorphicTypeValidator
- best fix = fixed DTO / schema-driven format
- never LaissezFaireSubTypeValidator
basics
~20 sDefault typing lets the JSON itself name a Java class to instantiate, so an attacker can name a 'gadget' class that does something harmful when built — potentially code execution. Remediate by disabling default typing and deserializing into fixed, known types.
solid answer
~40 sJackson's `ObjectMapper.enableDefaultTyping()` (and `activateDefaultTyping` in 2.10+) embeds a `@class`/`@type` field carrying a fully-qualified Java class name, and Jackson instantiates whatever it reads. Since the Kafka payload is attacker-controlled, the attacker chooses the class — a gadget chain (e.g. JNDI-lookup or template classes) reachable on the classpath gives remote code execution. Fixes, in order of preference: (1) remove polymorphic typing entirely and deserialize into a concrete DTO; (2) if polymorphism is genuinely required, use `activateDefaultTyping` with a `PolymorphicTypeValidator` (`BasicPolymorphicTypeValidator`) that allowlists a small set of trusted base types/subtypes, never `LaissezFaireSubTypeValidator`; (3) prefer schema-driven formats (Avro/Protobuf via Schema Registry) where the type is fixed by a registered schema, not by the payload. Also keep Jackson patched (the 'block-list' CVEs were a losing arms race) and bound payload size.
go deeper
Know that letting the message name a Java class to build is dangerous and should be avoided.
Identify enableDefaultTyping/activateDefaultTyping as the smell and propose deserializing into a fixed DTO.
Lay out the allowlist remediation with PolymorphicTypeValidator and explain why deny-lists fail; prefer schema-driven formats.
Set a policy banning payload-driven typing across services, choose registry-backed schemas as the standard, and reason about residual DoS and patch-management posture.
**What 'polymorphic default typing' means.** Normally Jackson deserializes JSON into a concrete target class you specify (`mapper.readValue(bytes, OrderEvent.class)`). 'Polymorphic typing' is for when a field is declared as an interface or base class and the concrete subtype must be recovered at runtime. To do that, Jackson writes an extra field — by default `@class` — holding the *fully-qualified Java class name*, and on read it does `Class.forName(thatName)` and instantiates it. 'Default typing' applies this to *all* eligible properties globally via `enableDefaultTyping()` (deprecated) / `activateDefaultTyping()`. **The vulnerability.** A Kafka value is attacker-controlled. If the deserializer obeys an embedded class name, the attacker decides which class gets loaded and constructed. They look for a 'gadget chain': classes already on the consumer's classpath whose construction or property-setting triggers a useful side effect. Classic examples instantiate a JNDI-backed datasource or a templating class that compiles and runs bytecode, yielding remote code execution (RCE). This is the same root cause as Java native-serialization RCE — the data stream is allowed to decide which types exist. **Why block-lists failed.** Jackson historically shipped a deny-list of known-dangerous classes (the long string of '2.x CVE' fixes). Attackers kept finding new gadgets, so the deny-list grew forever. The durable fix is an *allowlist*: explicitly enumerate the few types you trust. **Remediation, best to worst:** 1. **Eliminate polymorphism.** Most events have one concrete shape. Deserialize into a fixed DTO; the payload can no longer name a class. This is the strongest fix. 2. **Schema-driven format.** Move to Avro or Protobuf behind Confluent Schema Registry. The wire format references a *schema ID*, not a Java class name; the consumer maps that schema to known generated classes. The payload cannot introduce a new type. 3. **Constrained polymorphism.** If you truly need it, use `activateDefaultTyping(PolymorphicTypeValidator)` with a `BasicPolymorphicTypeValidator` configured to `allowIfSubType`/`allowIfBaseType` for a tiny, explicit set. Never use a permissive validator like `LaissezFaireSubTypeValidator`, and avoid `OBJECT_AND_NON_CONCRETE`/`NON_FINAL` defaulting without that validator. **Defense in depth.** Keep Jackson and `jackson-databind` patched; bound max payload and nesting depth to stop expansion/stack-overflow DoS; authenticate producers so the write surface is smaller; and wrap consumption with poison-pill handling so a crafted record fails closed rather than poisoning the consumer group. **Edge case.** Some teams set `@JsonTypeInfo` on specific fields rather than enabling it globally — that is narrower but still unsafe if the `Id.CLASS` mechanism is used without a validator. Prefer `Id.NAME` with a registered, finite set of subtypes (`@JsonSubTypes`).
- If the business genuinely needs polymorphic events, how do you keep it safe?Use `Id.NAME` with `@JsonSubTypes` to map a small fixed set of logical names to classes, or `activateDefaultTyping` with a `BasicPolymorphicTypeValidator` allowlisting only the trusted base/sub types. The payload then can only select among types you pre-approved, not arbitrary classpath classes.
- Why is keeping jackson-databind patched not sufficient on its own?Patches mostly extend a deny-list of known gadgets, which attackers keep circumventing with new chains. The structural fix is to not let the payload name classes at all (allowlist or no polymorphism); patching is only defense in depth.
saying these in an interview costs you the question
- Recommending the Jackson deny-list as the primary fix
- Saying default typing is fine 'because it's an internal topic'
- Using LaissezFaireSubTypeValidator to 'make it work'
- Confusing Id.NAME (safe, finite subtypes) with Id.CLASS (loads arbitrary classes)