Jackson Security Best Practices: Defending Against Deserialization Gadget Attacks

Jackson is powerful, but some of its older configuration options can open serious security vulnerabilities in your application. The most critical is polymorphic type handling: when configured carelessly, it allows an attacker to supply a JSON payload that causes Jackson to instantiate and invoke arbitrary Java classes on your server — a Remote Code Execution (RCE) vector known as “deserialization gadget attacks.” This guide explains the risk, shows you what to avoid, and demonstrates the safe patterns you should follow in all production code.

The Risk: enableDefaultTyping and Gadget Attacks

Jackson historically provided a method called enableDefaultTyping() that automatically embedded and honoured Java class names in serialised JSON. This means an attacker who can send JSON to your application could supply a class name of their choosing as the type discriminator, causing Jackson to instantiate that class during deserialisation.

The attack exploits classes already on the JVM classpath that have side effects in their constructors or setters — so-called “gadget classes.” Common culprits include JDBC drivers, logging libraries, and many Apache Commons classes.

Never use this in production:

// DANGEROUS — do not use in any application that receives untrusted JSON
mapper.enableDefaultTyping(); // Deprecated and removed in Jackson 2.16
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); // Also dangerous

Both of these methods were deprecated in Jackson 2.10 and completely removed in Jackson 3.x. If you encounter them in a codebase being migrated to Jackson 3, treat it as a blocker — the code will not compile and the security risk must be remediated before upgrading.

The Safe Alternative: PolymorphicTypeValidator

If you genuinely need default typing (for legacy object graphs or serialising dynamic containers), use activateDefaultTyping() with an explicit allowlist validator instead. Jackson 2.10 introduced BasicPolymorphicTypeValidator for this purpose:

// Build a validator that ONLY permits your own application packages
PolymorphicTypeValidator safeTypeValidator = BasicPolymorphicTypeValidator
    .builder()
    .allowIfBaseType(com.yourcompany.model.BasePayload.class) // Allow subclasses of this
    .allowIfSubType("com.yourcompany.model.")                 // Allow this package
    .build();

ObjectMapper mapper = new ObjectMapper();
mapper.activateDefaultTyping(
    safeTypeValidator,
    ObjectMapper.DefaultTyping.NON_FINAL,
    JsonTypeInfo.As.PROPERTY
);

This configuration will reject any type not explicitly permitted by the validator, stopping gadget-class attacks at the type-resolution stage.

Never Deserialise Untrusted JSON into Object.class

Deserialising untrusted JSON into Object.class (or Map<String, Object>) with default typing enabled is the root cause of most Jackson CVEs. Jackson will infer the type from whatever the JSON says:

// DANGEROUS with default typing enabled
Object result = mapper.readValue(untrustedJson, Object.class);

// SAFER: always target a specific, controlled type
MyRequestDto request = mapper.readValue(trustedJson, MyRequestDto.class);

Use @JsonTypeInfo Instead of Default Typing

The correct pattern for polymorphic deserialisation is to use @JsonTypeInfo with Id.NAME and an explicit @JsonSubTypes registry. This is safe because the permitted types are hardcoded at compile time — there is no way for an attacker to introduce an arbitrary class name:

// SAFE: only the two registered subtypes can ever be deserialised
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
    @JsonSubTypes.Type(value = EmailNotification.class,  name = "email"),
    @JsonSubTypes.Type(value = SmsNotification.class,    name = "sms")
})
public abstract class Notification {}

public class EmailNotification extends Notification {
    private String recipientEmail;
    // ...
}

public class SmsNotification extends Notification {
    private String recipientPhone;
    // ...
}

If someone sends {"type":"com.malicious.Gadget", ...}, Jackson will throw an InvalidTypeIdException because that name is not registered.

Jackson Security Best Practices Checklist

The following checklist consolidates every security recommendation from this guide into a single reference. Run through it during code reviews and security audits to verify your Jackson configuration is hardened correctly.

PracticeRecommendation
Default typingNever use enableDefaultTyping(). Use @JsonTypeInfo + @JsonSubTypes instead.
Deserialising untrusted inputAlways deserialise into a known, specific type, never into Object.
Unknown propertiesSet FAIL_ON_UNKNOWN_PROPERTIES=false to prevent failures on extra fields, but log unexpected inputs in high-security contexts.
Jackson versionKeep jackson-databind up to date. Many past CVEs were fixed in patch releases.
PolymorphicTypeValidatorIf you must use activateDefaultTyping(), always supply an explicit allowlist validator.
ObjectMapper exposureDo not expose a shared, mutable ObjectMapper to external code that could reconfigure it.

Keeping Jackson Up to Date

The Jackson project regularly publishes patch releases to address newly discovered gadget classes. Subscribe to the GitHub security advisories for jackson-databind and update promptly. A dependency management tool like Dependabot or Renovate makes this straightforward to automate.

See Also

AI Prompts for Jackson Security

Audit Jackson Config for Vulnerabilities

Here is my Jackson configuration class: [paste here]. Audit it for deserialization gadget attack vulnerabilities. Check for: enableDefaultTyping() or activateDefaultTyping() without a BasicPolymorphicTypeValidator, fields typed as Object.class or Serializable being deserialised from untrusted input, and default typing enabled globally on any shared mapper instance. For each finding, state the CVE or vulnerability class it relates to, the severity, and the exact code change that fixes it.

What it does: Scans the provided configuration for the three most common deserialization vulnerability patterns, maps each finding to a known CVE or vulnerability class, rates severity, and outputs a prioritised remediation list with the exact line-level code change required — not just general advice.

When to use it: During a security review, after a penetration test flags Jackson misconfiguration as a finding, or before upgrading to Jackson 3 (which removes enableDefaultTyping() entirely and will cause a compile error if it was present).

Replace enableDefaultTyping() Safely

My codebase uses enableDefaultTyping() on an ObjectMapper in these locations: [paste usages here]. This must be removed before upgrading to Jackson 3, which removes the method entirely. For each usage site, identify which classes are being deserialised polymorphically, generate the @JsonTypeInfo and @JsonSubTypes annotations for each affected class hierarchy, and show the updated mapper configuration with no global default typing. Include a unit test that verifies each hierarchy still round-trips correctly after the change.

What it does: Identifies every class deserialised polymorphically via default typing, applies @JsonTypeInfo(use=Id.NAME) and a fully typed @JsonSubTypes registry to each affected hierarchy, strips the unsafe mapper configuration, and generates a JUnit 5 test per hierarchy confirming correct deserialisation without any global type resolution.

When to use it: When remediating a CVE finding in an existing Jackson 2 application, or as a mandatory pre-step before upgrading to Jackson 3 — since enableDefaultTyping() does not exist in Jackson 3 and any call to it will fail to compile.

Build a PolymorphicTypeValidator Allowlist

My application needs default typing for this legacy object graph: [describe the graph or paste the classes]. Generate an activateDefaultTyping() call using BasicPolymorphicTypeValidator that restricts deserialisation to classes in these packages: [list packages] and below these base types: [list base types]. Show the validator builder chain, explain each allowlist entry, and write a negative test that verifies an arbitrary class name from outside the allowed packages is rejected with a JsonMappingException.

What it does: Produces a BasicPolymorphicTypeValidator.builder() chain scoped to the specified packages and base types, wires it into activateDefaultTyping(), and generates a negative-path JUnit 5 test that confirms a class outside the allowlist triggers a JsonMappingException rather than silently instantiating an arbitrary class.

When to use it: When there is a genuine architectural requirement for default typing on a legacy object graph — such as a plugin system or a long-lived serialised session store — and you must satisfy a security team’s requirement to prove the allowlist is enforced with a test, not just configured in code.

Conclusion

Jackson security comes down to two rules: never trust arbitrary class names embedded in JSON, and always deserialise into a known type with a controlled polymorphic registry. Remove any usage of enableDefaultTyping() from your codebase today, replace it with @JsonTypeInfo + @JsonSubTypes, keep your dependency current, and you will avoid the class of vulnerabilities that has generated the most Jackson CVEs in the past decade.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.