
CVE-2026-91776 is a jackson-databind availability issue in which many different unknown type identifiers can remain in a long-lived deserializer cache even though they all select the same fallback implementation. The FasterXML advisory identifies name-based polymorphism with a fallback, attacker-influenced type IDs, and reuse of a mapper or type deserializer as the important conditions. Ordinary use of Jackson to parse JSON is not enough to establish this path.
The useful response is to check both the resolved library version and the application configuration. Then verify a repaired build with a small, controlled regression that distinguishes repeated requests from distinct attacker-controlled keys. This is a cache-cardinality problem; a request-rate graph alone can miss the variable that makes the behavior different.
Which Jackson releases contain the fix
O CVE record’s affected packages and solutions distinguish the 2.x Maven coordinate com.fasterxml.jackson.core:jackson-databind from the 3.x coordinate tools.jackson.core:jackson-databind. Review the coordinate actually present in the application rather than matching only the artifact name.
| Package line in the record | Listed affected range | Listed fixed target |
|---|---|---|
| com.fasterxml.jackson.core:jackson-databind, older 2.x lines | 2.0.0 through 2.18.10 | 2.18.11 on the maintained 2.18 line |
| com.fasterxml.jackson.core:jackson-databind, intermediate 2.x lines | 2.19.0 through 2.21.6 | 2.21.7 |
| com.fasterxml.jackson.core:jackson-databind, 2.22 | 2.22.0 through 2.22.2 | 2.22.3 |
| tools.jackson.core:jackson-databind, early 3.x lines | 3.0.0 through 3.1.6 | 3.1.7 |
| tools.jackson.core:jackson-databind, 3.2 | 3.2.0 through 3.2.2 | 3.2.3 |
These are branch-specific targets, not an instruction to replace an application’s framework dependency set blindly. The record notes that several older branches received no fix on their own line. A team using one of those branches needs a supported migration or a documented distributor backport, with compatibility evidence for the rest of the application.
There is also an evidence distinction in the upstream advisory: the reporter runtime-confirmed the behavior on 2.22.1 and 3.2.1 while the package metadata gives broader affected ranges. Do not describe every historical release as independently reproduced. Use the published range for inventory decisions and keep any local reproduction claim tied to the versions actually tested.
Determine whether the application exposes the relevant path
Find the code and configuration that define polymorphic base types, accepted type names, and fallback behavior. The configuration example in the advisory uses JsonTypeInfo.Id.NAME with defaultImpl. Review equivalent behavior introduced through annotations, mapper configuration, custom resolvers, or framework integration. A text search is a discovery aid; someone must connect the matching code to the mapper that handles untrusted input.
| Condition to establish | Useful evidence | What a dependency scan alone misses |
|---|---|---|
| An affected jackson-databind build is present | Resolved dependencies and packaged artifact identity | Whether this is the library loaded by the running service |
| Name-based polymorphic resolution has a fallback | Relevant type declaration and mapper configuration | Whether unknown names are rejected or resolved |
| An untrusted party can influence the raw type ID | Input binding and trust-boundary review | Whether the value is fixed, validated, or externally controlled |
| The relevant type deserializer survives across requests | Application lifecycle and a controlled regression | Whether retained entries can accumulate over time |
| A repaired artifact has been deployed | Release provenance and running deployment identity | Whether changing a dependency file changed production |
Avoid two unhelpful shortcuts. Finding defaultImpl somewhere in a repository does not establish that a public route reaches it. Failing to find that literal annotation does not prove the behavior is absent, because configuration may be assembled indirectly. Trace the input to the actual deserialization entry point and record any gaps instead of inventing certainty.
Work with an application owner who understands the data contract. A fallback can exist to preserve compatibility with older producers or unknown event variants. Removing it changes how previously accepted data is handled. That may be an acceptable defensive change, but it needs an intentional compatibility decision rather than a repository-wide search-and-replace.
Why one fallback can still retain many keys
In the affected path, TypeDeserializerBase._findDeserializer resolves an unknown name to a fallback and records that result under the original raw identifier. The fallback object can be the same while the retained map keys differ. The upstream description frames the impact as incremental memory retention across requests, not a guaranteed single-request allocation spike or code execution.
For investigation, separate request count from key cardinality. Sending the same unknown identifier repeatedly asks the cache to reuse one key. Sending an equal number of distinct unknown identifiers asks it to retain different keys. A control that changes both the request count and identifier pattern cannot isolate which variable accounts for the observed difference.
This gives a practical interpretation of a quiet service that slowly retains more memory: the request rate may remain ordinary while the diversity of retained identifiers changes. It does not prove this CVE is the cause of that symptom. It suggests an additional measurement to test in a controlled environment, with the affected configuration and cache lifetime established first.

Try Penligent for Authorized Security Testing →
A bounded offline illustration of cache cardinality
This Python model uses sixteen synthetic identifiers and an in-memory dictionary. It does not import Jackson, send HTTP requests, allocate a large payload, or reproduce a production exploit. It demonstrates the measurement principle and contrasts it with a policy that does not retain unknown fallback resolutions. The toy policy is not a reimplementation of the vendor patch.
N = 16
known_names = {"known": object()}
fallback = object()
def run(type_ids, cache_unknown):
cache = {}
for raw_id in type_ids:
result = known_names.get(raw_id, fallback)
if raw_id in known_names or cache_unknown:
cache[raw_id] = result
return cache
repeated = ["unknown"] * N
distinct = [f"toy-{i}" for i in range(N)]
old_repeated = run(repeated, cache_unknown=True)
old_distinct = run(distinct, cache_unknown=True)
reviewed = run(distinct, cache_unknown=False)
known_control = run(["known"] * N, cache_unknown=False)
assert len(old_repeated) == 1
assert len(old_distinct) == N
assert len({id(v) for v in old_distinct.values()}) == 1
assert len(reviewed) == 0
assert len(known_control) == 1
print("Five bounded cardinality checks passed.")
The third assertion captures the counterintuitive part: the dictionary has multiple keys while every value refers to one object. Counting only the number of different fallback implementations would miss the retained key set. The repeated-input control is equally important because it shows why a successful same-input load test is weak evidence about a distinct-input condition.
Do not extrapolate this model into a bytes-per-identifier estimate for a JVM. Object layouts, strings, map internals, and application behavior differ. The model establishes a relationship between input diversity and retained dictionary entries in this small program. A real regression must establish what the actual repaired Jackson version and application configuration do.
Read the fixed implementation before defining the regression
O inspected 2.22.3 source makes an important distinction that a short vulnerability summary can hide. It avoids caching some unknown-ID handler results and nullifying fallback results under the raw identifier. Its comments also state that a defaultImpl deserializer can still be cached, with the cache bounded. A regression that insists every fallback-related cache is always empty would therefore test a stronger and different requirement than this implementation.
In that inspected release, the limits are expressed as 1,000 cached type IDs and 256 characters for a cacheable ID. The implementation skips caching an overlong identifier and clears the map when the size guard is reached before adding another entry. These are observed implementation details, not configuration advice, a universal promise for every release line, or a guarantee that total application memory is capped at a particular size.
The difference is operationally useful. The acceptance question is whether the relevant repaired path has the intended bounded retention behavior while preserving supported resolution. It is not whether a graph stays perfectly flat or a cache remains permanently empty. Capture which implementation and configuration were tested, because a handler path, nullifying fallback, and defaultImpl path need not have identical caching semantics.
The local Python model deliberately chooses a simple no-retention policy for unknown names. That is one way to illustrate the cardinality problem, not a description of every branch of the fixed Java code. Keep that boundary when explaining the example to reviewers. Otherwise a useful conceptual test can accidentally become an incorrect claim about a vendor implementation.
This is also a reason to avoid treating a dependency update as the entire regression specification. Choose cases that exercise the resolver and fallback the application actually uses. If the security summary and a source comment appear inconsistent, retain the discrepancy, inspect the selected release, and measure the relevant behavior rather than silently choosing the simpler explanation.
Inventory the resolved and packaged dependencies
For a Maven application, dependency-tree output can help locate the two coordinates. Run the query in an owned project with its normal repository policy. Maven may contact configured repositories; these are inventory commands, not evidence that a particular runtime path is reachable.
mvn dependency:tree -Dincludes=com.fasterxml.jackson.core:jackson-databind
mvn dependency:tree -Dincludes=tools.jackson.core:jackson-databind
Check the dependency management and framework release that selected the version, then inspect the application artifact through the team’s normal build-evidence process. The source declaration, resolved graph, packaged library, and running deployment are four different observations. A changed version in a dependency file is only the first step if the release was not rebuilt and rolled out.
For software supplied by another vendor, request the application release or documented package backport that addresses this CVE. Avoid replacing a bundled JAR by hand unless the vendor explicitly supports that procedure. An unsupported substitution can create a compatibility problem and leave the organization unable to explain which build is actually deployed.
When a fleet includes several applications, group by release artifact and configuration, not simply by team name. Two services built from similar source can use different dependency resolutions or mapper configuration. Keep the relevant evidence close to each deployment record so a fix in one service does not silently close findings in another.
Patch the cache behavior and preserve the data contract
O CVE record describes changes to fallback caching and limits on cached entries and cacheable identifier length. Verify the implementation delivered in the selected release rather than assuming every fallback path must produce an empty cache. The remediation target is the actual jackson-databind build, accompanied by a regression for the application’s supported inputs. Do not treat an HTTP body-size limit as an equivalent repair of a cache that persists across requests.
The record’s configuration mitigations include avoiding a catch-all fallback where the data shape allows it, or rejecting unknown names through a custom resolver before resolution. Shortening the mapper or deserializer lifetime limits accumulation but does not eliminate the issue. Those are temporary design decisions to evaluate, not substitutes for a supported fixed version.
| Response option | What it is intended to address | Tradeoff or remaining question |
|---|---|---|
| Upgrade to the fixed release for the application’s line | Repair the affected library behavior | Validate framework and data-contract compatibility |
| Reject unknown type names before fallback resolution | Remove the relevant untrusted name path | Confirm intended producers do not depend on unknown variants |
| Remove an unnecessary catch-all fallback | Make unsupported names fail explicitly | Review resulting error handling and compatibility behavior |
| Shorten mapper lifetime | Reduce how long retained state can accumulate | Does not eliminate the underlying condition and may add operational costs |
| Apply request limits | Reduce some input or traffic pressure | Does not itself establish a bound on process-lifetime key diversity |
| Restart the process | Recover current availability in some circumstances | Does not establish that the vulnerable behavior has been repaired |
Test errors as part of compatibility. If unknown names are newly rejected, verify that the application returns the intended response, avoids excessive logging of untrusted input, and does not repeatedly retry a permanently invalid message. A security change is more reliable when the surrounding workflow handles its new failure mode deliberately.
Design a matched regression rather than a stress attack
Use an isolated application environment with explicit stop conditions. Fix the library build, configuration, input count, identifier length, and deserializer lifetime for the first comparison. Vary only whether the unknown identifier is repeated or distinct. Include a known-type control so that an application that rejects every input does not appear repaired merely because nothing enters the intended path.
Choose a small input count sufficient to observe the relationship, not to exhaust a heap. Inspect retained-state evidence through the team’s approved instrumentation and record how that observation was obtained. If the cache cannot be inspected reliably, say so; a process-wide memory graph is supporting evidence with more possible explanations than a specific retained-entry observation.

Try Penligent for Authorized Security Testing →
| Regression case | Hold constant | Observation to interpret |
|---|---|---|
| Repeated unknown identifier | Input count, length, mapper lifetime, and fallback | Baseline for repeated resolution of one raw name |
| Distinct unknown identifiers | The same variables as the baseline | Whether retained state grows with distinct names |
| Supported known identifiers | Normal application data contract | Whether expected deserialization still works |
| Rejected unknown identifiers | Intended strict resolver configuration | Whether rejection happens before the unwanted retention path |
| Repaired build with the same cases | Test fixture and configuration | Whether the deployed repair changes the measured behavior |
Record the result separately for the vulnerable reference build, the proposed fixed build, and the deployed artifact. If no vulnerable reference build was tested, the exercise can still validate a repaired application, but it is not a local reproduction of the earlier flaw. Be precise about that distinction in the report.
A meaningful acceptance condition combines bounded retained-state behavior with correct processing of supported messages. Neither outcome should be inferred from the other. An application that avoids retention by discarding every message has broken its contract; an application that processes valid messages correctly may still retain unwanted state for a different class of input.
Interpret telemetry without overclaiming
For production observation, consider trends in retained heap after comparable activity, process restarts, error rates, and the mix of accepted and rejected type names where that information can be collected safely. These are investigation signals, not fingerprints that attribute a CVE by themselves. Keep raw untrusted values out of high-cardinality metric labels, or the measurement system can develop a separate cardinality problem.
Prefer bounded categories in operational dashboards: supported type, unknown type rejected, fallback used, and observation unavailable. If identifier-level samples are necessary, use a reviewed sampling and retention approach with access controls. Collect only what is needed to explain the behavior. Do not copy application secrets or full request bodies into a shared report just to demonstrate that an input was unusual.
Watch the application’s post-change behavior over a defined interval with a stated workload. A short quiet period is not comparable to a busy baseline, and a changed traffic mix can explain a different memory curve. Note those differences rather than treating every improvement as a controlled experiment. The strongest evidence remains a reproduced comparison with fixed inputs and known software provenance.
Keep scanner results tied to application evidence
The advisory’s availability impact is not a claim of Jackson gadget-chain RCE. Do not attach unrelated exploit payloads or broad deserialization warnings to this finding. Its name-based fallback and retained-key conditions deserve their own explanation, even when a scanner groups it under a general deserialization category.
For an authorized application assessment, a dependency match should lead to configuration review and a bounded retest. When evaluating Penligent or another automated workflow, ask which artifact and input path it observed and what evidence supports the proposed fix. External HTTP behavior alone may not reveal the lifetime or contents of an internal deserializer cache.
Close the software finding when the repaired artifact and relevant regression are established. Close any separate incident or availability investigation on its own evidence. If the team cannot determine which historical deployment handled a suspicious interval, record that limitation rather than converting a successful upgrade into a retrospective assurance.
Questions Java application owners ask
Does every Jackson application expose this CVE
No. A dependency inventory match warrants review, while the published conditions depend on polymorphic name resolution, fallback behavior, untrusted identifiers, and retained deserializer state.
Can JSON size limits alone solve the problem
They can constrain an individual request, but that is not the same as controlling the diversity of retained keys across requests. Keep such limits as defense in depth and verify the library repair.
Is this the same as Jackson remote code execution
No code-execution impact is asserted in the advisory reviewed here. The issue concerns availability pressure from retained cache entries. Do not infer a gadget chain from the word deserialization.
Can I fix it by creating a new mapper for every request
Changing lifetime can limit accumulation but is not the published software repair. Review performance and correctness implications with the application owner and upgrade to a supported fixed build.
Does the Python example prove our service was vulnerable
No. It demonstrates cache-key cardinality using sixteen local strings. It does not run Jackson, reach your input path, or measure your deployment. Keep the conceptual demonstration separate from application evidence.
What should a completed fix show
Retain the package coordinate and resolved version, mapper and fallback assessment, deployed artifact identity, bounded regression results, and correct handling of supported inputs. List any unresolved visibility or compatibility gap with an owner.
The central lesson of CVE-2026-91776 is that one semantic fallback can still create many retained keys. Repair the library, establish the relevant input and lifetime conditions, and compare matched cases at a small scale. That gives a more useful result than either a generic deserialization label or an aggressive load test that only demonstrates a service can be overwhelmed.

