From d7593b3bd1685980c0695bbb4a286d6179676070 Mon Sep 17 00:00:00 2001 From: Naveed Khan Date: Wed, 29 Jul 2026 23:43:23 +0530 Subject: [PATCH 1/2] suppress case variants of mapped names in isPropertySuppressed The check compared the raw expression token, while the mapped-descriptor fallback it guards derives its accessor names from the capitalized property name, so a name differing only in the case of its first character resolved the same accessors and was still readable and writable. --- .../beanutils2/MappedPropertyDescriptor.java | 2 +- .../commons/beanutils2/PropertyUtilsBean.java | 12 +++++++++--- .../commons/beanutils2/PropertyUtilsTest.java | 17 +++++++++++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/apache/commons/beanutils2/MappedPropertyDescriptor.java b/src/main/java/org/apache/commons/beanutils2/MappedPropertyDescriptor.java index 21d69d464..9a5b07e49 100644 --- a/src/main/java/org/apache/commons/beanutils2/MappedPropertyDescriptor.java +++ b/src/main/java/org/apache/commons/beanutils2/MappedPropertyDescriptor.java @@ -162,7 +162,7 @@ private Class reLoadClass(final String name) { * * @param s The property name */ - private static String capitalizePropertyName(final String s) { + static String capitalizePropertyName(final String s) { if (s.isEmpty()) { return s; } diff --git a/src/main/java/org/apache/commons/beanutils2/PropertyUtilsBean.java b/src/main/java/org/apache/commons/beanutils2/PropertyUtilsBean.java index be0f30665..b5303fd5f 100644 --- a/src/main/java/org/apache/commons/beanutils2/PropertyUtilsBean.java +++ b/src/main/java/org/apache/commons/beanutils2/PropertyUtilsBean.java @@ -1068,15 +1068,21 @@ private Object invokeMethod(final Method method, final Object bean, final Object /** * Tests whether a property name has been removed by a registered {@link SuppressPropertiesBeanIntrospector}. The mapped-descriptor fallback in * {@link #getPropertyDescriptor(Object, String)} bypasses the introspection pipeline, so suppressed mapped property names must be filtered explicitly. + * {@link MappedPropertyDescriptor} derives its accessor names from the capitalized property name, so names differing only in the case of their first + * character resolve the same accessors and are compared on that capitalized form here. * * @param name The property name to test. * @return {@code true} if the name is suppressed by an introspector. */ private boolean isPropertySuppressed(final String name) { + final String base = MappedPropertyDescriptor.capitalizePropertyName(name); for (final BeanIntrospector introspector : introspectors) { - if (introspector instanceof SuppressPropertiesBeanIntrospector - && ((SuppressPropertiesBeanIntrospector) introspector).getSuppressedProperties().contains(name)) { - return true; + if (introspector instanceof SuppressPropertiesBeanIntrospector) { + for (final String suppressed : ((SuppressPropertiesBeanIntrospector) introspector).getSuppressedProperties()) { + if (base.equals(MappedPropertyDescriptor.capitalizePropertyName(suppressed))) { + return true; + } + } } } return false; diff --git a/src/test/java/org/apache/commons/beanutils2/PropertyUtilsTest.java b/src/test/java/org/apache/commons/beanutils2/PropertyUtilsTest.java index 683389508..fb6c1aee5 100644 --- a/src/test/java/org/apache/commons/beanutils2/PropertyUtilsTest.java +++ b/src/test/java/org/apache/commons/beanutils2/PropertyUtilsTest.java @@ -315,6 +315,23 @@ void testCustomIntrospectionSuppressedMappedProperty() throws Exception { assertEquals("First Value", unsuppressed.getProperty(bean, "mappedProperty(First Key)"), "Mapped property should be readable when not suppressed"); } + /** + * {@link MappedPropertyDescriptor} capitalizes the first character of the property name to build the accessor names, so {@code MappedProperty(key)} + * resolves the same {@code getMappedProperty}/{@code setMappedProperty} pair as {@code mappedProperty(key)} and must be suppressed with it. + */ + @Test + void testCustomIntrospectionSuppressedMappedPropertyCaseVariant() throws Exception { + final PropertyUtilsBean pub = new PropertyUtilsBean(); + pub.addBeanIntrospector(new SuppressPropertiesBeanIntrospector(Arrays.asList("mappedProperty"))); + + assertNull(pub.getPropertyDescriptor(bean, "MappedProperty"), "Case variant of a suppressed mapped property should have no descriptor"); + assertThrows(NoSuchMethodException.class, () -> pub.getProperty(bean, "MappedProperty(First Key)"), + "Case variant of a suppressed mapped property must not be readable"); + assertThrows(NoSuchMethodException.class, () -> pub.setProperty(bean, "MappedProperty(First Key)", "changed"), + "Case variant of a suppressed mapped property must not be writable"); + assertEquals("First Value", bean.getMappedProperty("First Key"), "Suppressed mapped property must be unchanged"); + } + /** * Test the describe() method. */ From dbed3495e77172c5945db93cd568faee1e459ef1 Mon Sep 17 00:00:00 2001 From: Naveed Khan Date: Thu, 30 Jul 2026 13:53:51 +0530 Subject: [PATCH 2/2] skip null suppressed names in isPropertySuppressed SuppressPropertiesBeanIntrospector only rejects a null collection, so the suppressed set can hold null entries. Capitalizing them threw a NullPointerException where the previous Set.contains just never matched. --- .../commons/beanutils2/PropertyUtilsBean.java | 2 +- .../commons/beanutils2/PropertyUtilsTest.java | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/apache/commons/beanutils2/PropertyUtilsBean.java b/src/main/java/org/apache/commons/beanutils2/PropertyUtilsBean.java index b5303fd5f..36c653f07 100644 --- a/src/main/java/org/apache/commons/beanutils2/PropertyUtilsBean.java +++ b/src/main/java/org/apache/commons/beanutils2/PropertyUtilsBean.java @@ -1079,7 +1079,7 @@ private boolean isPropertySuppressed(final String name) { for (final BeanIntrospector introspector : introspectors) { if (introspector instanceof SuppressPropertiesBeanIntrospector) { for (final String suppressed : ((SuppressPropertiesBeanIntrospector) introspector).getSuppressedProperties()) { - if (base.equals(MappedPropertyDescriptor.capitalizePropertyName(suppressed))) { + if (suppressed != null && base.equals(MappedPropertyDescriptor.capitalizePropertyName(suppressed))) { return true; } } diff --git a/src/test/java/org/apache/commons/beanutils2/PropertyUtilsTest.java b/src/test/java/org/apache/commons/beanutils2/PropertyUtilsTest.java index fb6c1aee5..8e10a0ecf 100644 --- a/src/test/java/org/apache/commons/beanutils2/PropertyUtilsTest.java +++ b/src/test/java/org/apache/commons/beanutils2/PropertyUtilsTest.java @@ -332,6 +332,20 @@ void testCustomIntrospectionSuppressedMappedPropertyCaseVariant() throws Excepti assertEquals("First Value", bean.getMappedProperty("First Key"), "Suppressed mapped property must be unchanged"); } + /** + * {@link SuppressPropertiesBeanIntrospector} only rejects a null collection, so the set of suppressed names may hold null entries. Those must be skipped + * rather than capitalized when the mapped-descriptor fallback is checked. + */ + @Test + void testCustomIntrospectionSuppressedMappedPropertyNullEntry() throws Exception { + final PropertyUtilsBean pub = new PropertyUtilsBean(); + pub.addBeanIntrospector(new SuppressPropertiesBeanIntrospector(Arrays.asList(null, "mappedProperty"))); + + assertNull(pub.getPropertyDescriptor(bean, "MappedProperty"), "Case variant of a suppressed mapped property should have no descriptor"); + assertNull(pub.getPropertyDescriptor(bean, "mappedProperty"), "Suppressed mapped property should have no descriptor"); + assertNotNull(pub.getPropertyDescriptor(bean, "stringProperty"), "A null suppressed entry must not hide unrelated properties"); + } + /** * Test the describe() method. */