CVE-2017-7525 is the CVE that made Java developers take jackson-databind seriously as an attack surface, and it did not stay a one-off. When FasterXML's default-typing feature is enabled, ObjectMapper.readValue() reads a type identifier — typically a @class property — straight out of the incoming JSON and uses it to decide which Java class to instantiate. If an attacker controls that JSON and a "gadget" class from a library like Commons-Collections, Groovy, C3P0, or Spring sits anywhere on the classpath, deserialization alone can trigger constructor and setter side effects that chain into remote code execution — the same class of attack long known from native Java serialization, just reached through JSON instead of a binary stream. According to jackson-databind maintainer Tatu Saloranta (cowtowncoder), FasterXML's initial response was to blacklist specific bad classes one at a time, and that approach alone produced roughly 30 additional CVEs as researchers kept finding new gadgets that weren't yet on the list. Jackson 2.10 finally addressed the root cause with a real allow-list mechanism instead of an ever-growing deny-list. This post walks through why the vulnerability class exists, how it evolved, and how to configure ObjectMapper so untrusted input can't pick your classes for you.
What actually makes polymorphic deserialization dangerous?
Polymorphic deserialization is dangerous because it inverts a basic trust assumption: normally your code decides what class to instantiate, but with default typing enabled, the caller's JSON payload decides. Jackson supports this on purpose — it's how a field typed as an interface or abstract class can be deserialized back into the correct concrete subtype without every consumer hardcoding a switch statement. The mechanism is @JsonTypeInfo, and its most permissive settings, Id.CLASS and Id.MINIMAL_CLASS, embed the fully-qualified class name directly in the JSON and trust it verbatim. Before Jackson 2.10, developers commonly enabled this globally with the now-deprecated enableDefaultTyping() call, which applied unchecked polymorphism to every object the mapper touched, not just fields that needed it. If any class reachable on the classpath had a constructor, setter, or readObject/finalize-style hook with an exploitable side effect — writing a file, opening a JNDI lookup, invoking a shell — an attacker who could reach readValue() with arbitrary JSON had a path to that side effect without needing any other bug in the target application.
Why did one CVE turn into nearly thirty?
Because FasterXML's early fix strategy treated each gadget class as an isolated bug rather than treating unchecked type resolution as the actual vulnerability. Saloranta has described the pattern directly on the jackson-databind issue tracker and in his own writing on the project's CVE history: after CVE-2017-7525, the library added specific classes to an internal blockedClassNames denylist as researchers reported them, and each newly discovered gadget triggered a fresh CVE filing even though the underlying weakness — unrestricted, JSON-controlled class instantiation — never changed. A denylist can only block gadgets someone has already found; it does nothing against a gadget class from a dependency nobody has audited yet. That dynamic played out repeatedly through 2018 and 2019 as researchers cycled through libraries such as Groovy, Spring, and various JNDI-capable connection pools, each yielding its own CVE against jackson-databind even though the fix, structurally, was the same denylist addition every time.
What changed with Jackson 2.10's "safe default typing"?
Jackson 2.10, released in 2019, replaced the reactive denylist with a proactive allow-list model built around a new abstract class, PolymorphicTypeValidator, and a concrete builder implementation, BasicPolymorphicTypeValidator. Instead of asking "is this class known-bad," the validator asks "is this class explicitly permitted," which flips the default from trust-unless-blocked to deny-unless-allowed. Applications adopt it through new activateDefaultTyping(PolymorphicTypeValidator, DefaultTyping, ...) overloads on ObjectMapper, replacing the older unchecked enableDefaultTyping() calls that took no validator argument at all. BasicPolymorphicTypeValidator.builder() lets you constrain acceptable types by base class, exact subtype, or package name, so a mapper handling, say, a Shape hierarchy can be told to accept only classes that extend Shape and live in your own com.yourapp.model package — a gadget class from an unrelated library simply doesn't match the rule and is rejected before instantiation, regardless of whether anyone has ever reported it as a known-bad class.
How should teams actually configure ObjectMapper?
The safest configuration avoids global default typing entirely. Scope polymorphism to the specific fields that need it using @JsonTypeInfo paired with an explicit @JsonSubTypes list naming every legal subtype, rather than Id.CLASS or Id.MINIMAL_CLASS, which accept arbitrary classpath names. If a use case genuinely requires broader default typing — some frameworks and generic caching layers do — always pair activateDefaultTyping() with a strict BasicPolymorphicTypeValidator built with allow rules, never a validator that merely subtracts known-bad classes. Just as importantly, treat any ObjectMapper configured for default typing, safe or not, as equivalent in risk to native Java ObjectInputStream deserialization: never point it at input that crosses a trust boundary — request bodies, message-queue payloads, uploaded files — without validator-scoped typing in place. And because new gadget classes keep surfacing in third-party libraries independent of Jackson's own code, keeping jackson-databind itself current still matters even with a validator configured, since the library's own fixes continue to close edge cases the allow-list doesn't fully anticipate.
How does this connect to dependency management?
Most organizations don't write the vulnerable ObjectMapper configuration themselves — they inherit it through a transitive dependency, an older internal library, or a framework default that predates Jackson 2.10. That makes version visibility as important as code review here: a project pinned to a jackson-databind release from before the 2.10 line simply doesn't have PolymorphicTypeValidator available to it at all, no matter how carefully the application code is written elsewhere. Software composition analysis and SBOM tooling that tracks resolved dependency versions across a build — including transitive ones pulled in by unrelated frameworks — gives a team a fast, concrete answer to "which of our services are even running a jackson-databind version old enough for this to matter," which is the first question worth answering before auditing every @JsonTypeInfo annotation by hand.