From 8690032d7895f1864826731ae06f1d55165b0e28 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Tue, 5 May 2026 18:35:29 -0700 Subject: [PATCH] Fix #5962: Case-insensitive deserialization may use wrong `@JsonIgnoreProperties` --- release-notes/CREDITS | 3 + release-notes/VERSION | 2 + .../deser/bean/BeanDeserializerBase.java | 5 +- ...norePropertiesCaseInsensitive5962Test.java | 92 +++++++++++++++++++ 4 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 src/test/java/tools/jackson/databind/deser/filter/IgnorePropertiesCaseInsensitive5962Test.java diff --git a/release-notes/CREDITS b/release-notes/CREDITS index ed117c3d03..c6fc8bb095 100644 --- a/release-notes/CREDITS +++ b/release-notes/CREDITS @@ -345,6 +345,9 @@ Omkhar Arasaratnam (@omkhar) [3.1.4] * Contributed fix for #5957: Improve `java.time.Month` deserialization validation [3.1.4] + * Contributed fix for #5962: Case-insensitive deserialization may use + wrong `@JsonIgnoreProperties` + [3.1.4] Michael Orzechowski (@MikeBlink) * Reported #5941: `MapperBuilder.addModule()` does not recursively register transitive diff --git a/release-notes/VERSION b/release-notes/VERSION index d90b212663..4469a2bb05 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -19,6 +19,8 @@ No changes since 3.1 (reported, fix suggested by Omkhar A) #5957: Improve `java.time.Month` deserialization validation (fix by Omkhar A) +#5962: Case-insensitive deserialization may use wrong `@JsonIgnoreProperties` + (fixed by Omkhar A) 3.1.3 (01-May-2026) diff --git a/src/main/java/tools/jackson/databind/deser/bean/BeanDeserializerBase.java b/src/main/java/tools/jackson/databind/deser/bean/BeanDeserializerBase.java index f0eb920acb..49c3469804 100644 --- a/src/main/java/tools/jackson/databind/deser/bean/BeanDeserializerBase.java +++ b/src/main/java/tools/jackson/databind/deser/bean/BeanDeserializerBase.java @@ -866,7 +866,10 @@ public ValueDeserializer createContextual(DeserializationContext ctxt, // 16-May-2016, tatu: How about per-property case-insensitivity? Boolean B = format.getFeature(JsonFormat.Feature.ACCEPT_CASE_INSENSITIVE_PROPERTIES); if (B != null) { - BeanPropertyMap propsOrig = _beanProperties; + // [databind#5962]: must rebuild from the (possibly filtered) contextual + // BeanPropertyMap so that per-property @JsonIgnoreProperties exclusions + // applied by _handleByNameInclusion() above are preserved. + BeanPropertyMap propsOrig = contextual._beanProperties; BeanPropertyMap props = propsOrig.withCaseInsensitivity(B.booleanValue()); if (props != propsOrig) { contextual = contextual.withBeanProperties(props); diff --git a/src/test/java/tools/jackson/databind/deser/filter/IgnorePropertiesCaseInsensitive5962Test.java b/src/test/java/tools/jackson/databind/deser/filter/IgnorePropertiesCaseInsensitive5962Test.java new file mode 100644 index 0000000000..1fa25c37d7 --- /dev/null +++ b/src/test/java/tools/jackson/databind/deser/filter/IgnorePropertiesCaseInsensitive5962Test.java @@ -0,0 +1,92 @@ +package tools.jackson.databind.deser.filter; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import tools.jackson.databind.*; +import tools.jackson.databind.testutil.DatabindTestUtil; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * [databind#5962]: Case-insensitive BeanPropertyMap rebuild undoes per-property + * {@code @JsonIgnoreProperties}. + * + * {@code BeanDeserializerBase.createContextual()} calls {@code _handleByNameInclusion()} + * to filter properties according to per-property {@code @JsonIgnoreProperties}, producing + * a contextual deserializer with the restricted {@code BeanPropertyMap}. However, the + * subsequent case-insensitivity block read {@code _beanProperties} (the *original* + * unfiltered map from {@code this}) rather than {@code contextual._beanProperties} (the + * filtered map). {@code withCaseInsensitivity()} then rebuilt the map from the unfiltered + * source, and {@code contextual.withBeanProperties(props)} overwrote the filtered map with + * the unfiltered one — any properties removed by {@code _handleByNameInclusion} were + * restored. + * + * Patch: source the case-insensitive rebuild from {@code contextual._beanProperties}. + */ +public class IgnorePropertiesCaseInsensitive5962Test extends DatabindTestUtil +{ + static class AdminDto { + public String adminKey = "DEFAULT"; + public String username; + } + + // Container that ignores "adminKey" on the AdminDto field AND enables case-insensitive matching + static class Container { + @JsonIgnoreProperties("adminKey") + @JsonFormat(with = JsonFormat.Feature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + public AdminDto admin; + } + + // Baseline container: only @JsonIgnoreProperties, no case-insensitive format override + static class BaselineContainer { + @JsonIgnoreProperties("adminKey") + public AdminDto admin; + } + + /** + * NEGATIVE CONTROL: without the @JsonFormat case-insensitive override, @JsonIgnoreProperties + * correctly suppresses adminKey on the nested AdminDto field. + */ + @Test + public void test5962_negativeControl_withoutCaseInsensitivity() throws Exception { + ObjectMapper mapper = jsonMapperBuilder().build(); + String json = "{\"admin\":{\"adminKey\":\"HACKED\",\"username\":\"alice\"}}"; + BaselineContainer result = mapper.readValue(json, BaselineContainer.class); + // Without case-insensitive format, @JsonIgnoreProperties blocks adminKey + assertNotEquals("HACKED", result.admin.adminKey, + "@JsonIgnoreProperties alone (no case-insensitive format) should block adminKey"); + assertEquals("alice", result.admin.username); + } + + /** + * EXPLOIT PATH: the case-insensitive BeanPropertyMap rebuild (triggered by + * @JsonFormat ACCEPT_CASE_INSENSITIVE_PROPERTIES) restores the unfiltered original + * _beanProperties, undoing the @JsonIgnoreProperties("adminKey") exclusion. + * Case-insensitive matching then routes "adminKey" (or "ADMINKEY") to the setter. + * + * Security assertion: adminKey must NOT be settable via JSON when the enclosing + * container declares @JsonIgnoreProperties("adminKey") on the field. + */ + @Test + public void test5962_caseInsensitiveRebuildRestoresIgnoredProperty() throws Exception { + ObjectMapper mapper = jsonMapperBuilder().build(); + + // Exact case — should be blocked by @JsonIgnoreProperties + String json = "{\"admin\":{\"adminKey\":\"HACKED\",\"username\":\"alice\"}}"; + Container result = mapper.readValue(json, Container.class); + assertNotEquals("HACKED", result.admin.adminKey, + "[databind#5962]: case-insensitive BeanPropertyMap rebuild restored 'adminKey' " + + "after it was removed by @JsonIgnoreProperties. The property was set to 'HACKED'."); + assertEquals("alice", result.admin.username); + + // Mixed case — exploits the case-insensitive rebuild more directly + String jsonMixed = "{\"admin\":{\"AdminKey\":\"HACKED2\",\"username\":\"bob\"}}"; + Container result2 = mapper.readValue(jsonMixed, Container.class); + assertNotEquals("HACKED2", result2.admin.adminKey, + "[databind#5962]: 'AdminKey' (mixed case) matched 'adminKey' via case-insensitive " + + "lookup that was rebuilt from the unfiltered property map."); + } +}