Skip to content

Commit 19cc496

Browse files
committed
Fix #5962: Case-insensitive deserialization may use wrong @JsonIgnoreProperties
1 parent 62a7d09 commit 19cc496

4 files changed

Lines changed: 101 additions & 1 deletion

File tree

‎release-notes/CREDITS‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,9 @@ Omkhar Arasaratnam (@omkhar)
366366
* Contributed fix for #5958: `@JsonView` ignored with
367367
`@JsonTypeInfo(include = As.EXTERNAL_PROPERTY)`
368368
[3.2.0]
369+
* Contributed fix for #5962: Case-insensitive deserialization may use
370+
wrong `@JsonIgnoreProperties`
371+
[3.2.0]
369372

370373
Michael Orzechowski (@MikeBlink)
371374
* Reported #5941: `MapperBuilder.addModule()` does not recursively register transitive

‎release-notes/VERSION‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,8 @@ Versions: 3.x (for earlier see VERSION-2.x)
213213
#5958: `@JsonView` ignored with `@JsonTypeInfo(include = As.EXTERNAL_PROPERTY)`
214214
(fixed by Omkhar A)
215215
#5960: Deprecate un-maintained `tools.jackson.databind.jsontype.impl.SubTypeValidator`
216+
#5962: Case-insensitive deserialization may use wrong `@JsonIgnoreProperties`
217+
(fixed by Omkhar A)
216218
- Remove project-specific Android SDK compatibility level, use shared;
217219
no change level itself (stillAndroid SDK 34)
218220

‎src/main/java/tools/jackson/databind/deser/bean/BeanDeserializerBase.java‎

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

0 commit comments

Comments
 (0)