Skip to content

Commit c64d3c8

Browse files
pstreefjkschneider
authored andcommitted
backpatch CVE-2026-54515: keep per-property @JsonIgnoreProperties under case-insensitive matching
Port of upstream bc1613c ("Backport PR FasterXML#5964 into 2.18 to fix FasterXML#5962", FasterXML#6039), shipped in 2.18.9, onto the 2.13.5 baseline. BeanDeserializerBase.createContextual() first calls _handleByNameInclusion() to apply the per-property @JsonIgnoreProperties exclusions, producing a contextual deserializer whose BeanPropertyMap no longer carries the ignored properties. If the property ALSO carries @jsonformat(with = Feature.ACCEPT_CASE_INSENSITIVE_PROPERTIES), the block that follows rebuilt the case-insensitive map from `_beanProperties` -- this deserializer's ORIGINAL, unfiltered map -- and then installed it on the contextual one. Every property the exclusion had just removed came back, and case-insensitive lookup then routed the incoming field to it. A declared @JsonIgnoreProperties("adminKey") stopped holding, which is a write to a field the application had declared off-limits to JSON. The fix reads contextual._beanProperties instead, so the rebuild starts from the filtered map. NOT A CHERRY-PICK: the commit also edits release-notes/VERSION-2.x and CREDITS-2.x, which have no 2.18 section at this baseline and conflict. The production hunk merged with no conflict and is upstream's bytes -- one changed line plus upstream's three-line comment. Binary compatibility: no API change of any kind; one expression inside an existing method body. BEHAVIOUR CHANGE: a property excluded by a per-property @JsonIgnoreProperties stays excluded when case-insensitive matching is enabled on the same property. A consumer that (knowingly or not) depended on the property being writable again will now see it ignored -- which is the CVE. Regression gate: IgnorePropertiesCaseInsensitive5962Test, upstream's own file for the issue, converted from JUnit 5 on DatabindTestUtil (neither exists at this baseline) to JUnit 3 on BaseMapTest: @test dropped, methods renamed to the testXxx form, and assertNotEquals rewritten in JUnit 4's (message, unexpected, actual) argument order. The DTOs, payloads and asserted values are upstream's, including its own negative control -- the same document against a container WITHOUT the case-insensitive format override, which passes on the unpatched baseline and is what shows the exclusion mechanism itself was never broken. Upstream-Commit: bc1613c
1 parent 9c201c6 commit c64d3c8

2 files changed

Lines changed: 93 additions & 1 deletion

File tree

‎src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBase.java‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -808,7 +808,10 @@ public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
808808
// 16-May-2016, tatu: How about per-property case-insensitivity?
809809
Boolean B = format.getFeature(JsonFormat.Feature.ACCEPT_CASE_INSENSITIVE_PROPERTIES);
810810
if (B != null) {
811-
BeanPropertyMap propsOrig = _beanProperties;
811+
// [databind#5962]: must rebuild from the (possibly filtered) contextual
812+
// BeanPropertyMap so that per-property @JsonIgnoreProperties exclusions
813+
// applied by _handleByNameInclusion() above are preserved.
814+
BeanPropertyMap propsOrig = contextual._beanProperties;
812815
BeanPropertyMap props = propsOrig.withCaseInsensitivity(B.booleanValue());
813816
if (props != propsOrig) {
814817
contextual = contextual.withBeanProperties(props);
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package com.fasterxml.jackson.databind.deser.filter;
2+
3+
import com.fasterxml.jackson.annotation.JsonFormat;
4+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
5+
6+
import com.fasterxml.jackson.databind.*;
7+
8+
import static org.junit.Assert.assertNotEquals;
9+
10+
/**
11+
* [databind#5962]: Case-insensitive BeanPropertyMap rebuild undoes per-property
12+
* {@code @JsonIgnoreProperties}.
13+
*
14+
* {@code BeanDeserializerBase.createContextual()} calls {@code _handleByNameInclusion()}
15+
* to filter properties according to per-property {@code @JsonIgnoreProperties}, producing
16+
* a contextual deserializer with the restricted {@code BeanPropertyMap}. However, the
17+
* subsequent case-insensitivity block read {@code _beanProperties} (the *original*
18+
* unfiltered map from {@code this}) rather than {@code contextual._beanProperties} (the
19+
* filtered map). {@code withCaseInsensitivity()} then rebuilt the map from the unfiltered
20+
* source, and {@code contextual.withBeanProperties(props)} overwrote the filtered map with
21+
* the unfiltered one — any properties removed by {@code _handleByNameInclusion} were
22+
* restored.
23+
*
24+
* Patch: source the case-insensitive rebuild from {@code contextual._beanProperties}.
25+
*/
26+
public class IgnorePropertiesCaseInsensitive5962Test extends BaseMapTest
27+
{
28+
static class AdminDto {
29+
public String adminKey = "DEFAULT";
30+
public String username;
31+
}
32+
33+
// Container that ignores "adminKey" on the AdminDto field AND enables case-insensitive matching
34+
static class Container {
35+
@JsonIgnoreProperties("adminKey")
36+
@JsonFormat(with = JsonFormat.Feature.ACCEPT_CASE_INSENSITIVE_PROPERTIES)
37+
public AdminDto admin;
38+
}
39+
40+
// Baseline container: only @JsonIgnoreProperties, no case-insensitive format override
41+
static class BaselineContainer {
42+
@JsonIgnoreProperties("adminKey")
43+
public AdminDto admin;
44+
}
45+
46+
/**
47+
* NEGATIVE CONTROL: without the @JsonFormat case-insensitive override, @JsonIgnoreProperties
48+
* correctly suppresses adminKey on the nested AdminDto field.
49+
*/
50+
public void testNegativeControlWithoutCaseInsensitivity() throws Exception {
51+
ObjectMapper mapper = jsonMapperBuilder().build();
52+
String json = "{\"admin\":{\"adminKey\":\"HACKED\",\"username\":\"alice\"}}";
53+
BaselineContainer result = mapper.readValue(json, BaselineContainer.class);
54+
// Without case-insensitive format, @JsonIgnoreProperties blocks adminKey
55+
assertNotEquals("@JsonIgnoreProperties alone (no case-insensitive format) should block adminKey",
56+
"HACKED", result.admin.adminKey);
57+
assertEquals("alice", result.admin.username);
58+
}
59+
60+
/**
61+
* EXPLOIT PATH: the case-insensitive BeanPropertyMap rebuild (triggered by
62+
* @JsonFormat ACCEPT_CASE_INSENSITIVE_PROPERTIES) restores the unfiltered original
63+
* _beanProperties, undoing the @JsonIgnoreProperties("adminKey") exclusion.
64+
* Case-insensitive matching then routes "adminKey" (or "ADMINKEY") to the setter.
65+
*
66+
* Security assertion: adminKey must NOT be settable via JSON when the enclosing
67+
* container declares @JsonIgnoreProperties("adminKey") on the field.
68+
*/
69+
public void testCaseInsensitiveRebuildRestoresIgnoredProperty() throws Exception {
70+
ObjectMapper mapper = jsonMapperBuilder()
71+
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
72+
.build();
73+
74+
// Exact case — should be blocked by @JsonIgnoreProperties
75+
String json = "{\"admin\":{\"adminKey\":\"HACKED\",\"username\":\"alice\"}}";
76+
Container result = mapper.readValue(json, Container.class);
77+
assertNotEquals("[databind#5962]: case-insensitive BeanPropertyMap rebuild restored 'adminKey' " +
78+
"after it was removed by @JsonIgnoreProperties. The property was set to 'HACKED'.",
79+
"HACKED", result.admin.adminKey);
80+
assertEquals("alice", result.admin.username);
81+
82+
// Mixed case — exploits the case-insensitive rebuild more directly
83+
String jsonMixed = "{\"admin\":{\"AdminKey\":\"HACKED2\",\"username\":\"bob\"}}";
84+
Container result2 = mapper.readValue(jsonMixed, Container.class);
85+
assertNotEquals("[databind#5962]: 'AdminKey' (mixed case) matched 'adminKey' via case-insensitive " +
86+
"lookup that was rebuilt from the unfiltered property map.",
87+
"HACKED2", result2.admin.adminKey);
88+
}
89+
}

0 commit comments

Comments
 (0)