From fd220124926ff0c6e760d5331eb62e09b20b46a9 Mon Sep 17 00:00:00 2001 From: Marian Koncek Date: Wed, 18 Dec 2019 16:01:16 +0100 Subject: [PATCH] CVE-2014-0114 --- .../beanutils/BeanIntrospectionData.java | 154 +++++++++++++ .../commons/beanutils/BeanIntrospector.java | 53 +++++ .../beanutils/DefaultBeanIntrospector.java | 181 +++++++++++++++ .../DefaultIntrospectionContext.java | 108 +++++++++ .../beanutils/IntrospectionContext.java | 99 ++++++++ .../commons/beanutils/PropertyUtils.java | 15 ++ .../commons/beanutils/PropertyUtilsBean.java | 213 +++++++++--------- .../SuppressPropertiesBeanIntrospector.java | 75 ++++++ ...essPropertiesBeanIntrospectorTestCase.java | 127 +++++++++++ .../beanutils/bugs/Jira463TestCase.java | 48 ++++ .../beanutils/bugs/Jira520TestCase.java | 39 ++++ 11 files changed, 1000 insertions(+), 112 deletions(-) create mode 100644 src/main/java/org/apache/commons/beanutils/BeanIntrospectionData.java create mode 100644 src/main/java/org/apache/commons/beanutils/BeanIntrospector.java create mode 100644 src/main/java/org/apache/commons/beanutils/DefaultBeanIntrospector.java create mode 100644 src/main/java/org/apache/commons/beanutils/DefaultIntrospectionContext.java create mode 100644 src/main/java/org/apache/commons/beanutils/IntrospectionContext.java create mode 100644 src/main/java/org/apache/commons/beanutils/SuppressPropertiesBeanIntrospector.java create mode 100644 src/test/java/org/apache/commons/beanutils/SuppressPropertiesBeanIntrospectorTestCase.java create mode 100644 src/test/java/org/apache/commons/beanutils/bugs/Jira463TestCase.java create mode 100644 src/test/java/org/apache/commons/beanutils/bugs/Jira520TestCase.java diff --git a/src/main/java/org/apache/commons/beanutils/BeanIntrospectionData.java b/src/main/java/org/apache/commons/beanutils/BeanIntrospectionData.java new file mode 100644 index 0000000..1ab1d1e --- /dev/null +++ b/src/main/java/org/apache/commons/beanutils/BeanIntrospectionData.java @@ -0,0 +1,154 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.beanutils; + +import java.beans.IntrospectionException; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +/** + *

+ * An internally used helper class for storing introspection information about a bean + * class. + *

+ *

+ * This class is used by {@link PropertyUtilsBean}. When accessing bean properties via + * reflection information about the properties available and their types and access + * methods must be present. {@code PropertyUtilsBean} stores this information in a cache + * so that it can be accessed quickly. The cache stores instances of this class. + *

+ *

+ * This class mainly stores information about the properties of a bean class. Per default, + * this is contained in {@code PropertyDescriptor} objects. Some additional information + * required by the {@code BeanUtils} library is also stored here. + *

+ * + * @version $Id$ + * @since 1.9.1 + */ +class BeanIntrospectionData { + /** An array with property descriptors for the managed bean class. */ + private final PropertyDescriptor[] descriptors; + + /** A map for remembering the write method names for properties. */ + private final Map writeMethodNames; + + /** + * Creates a new instance of {@code BeanIntrospectionData} and initializes its + * completely. + * + * @param descs the array with the descriptors of the available properties + */ + public BeanIntrospectionData(final PropertyDescriptor[] descs) { + this(descs, setUpWriteMethodNames(descs)); + } + + /** + * Creates a new instance of {@code BeanIntrospectionData} and allows setting the map + * with write method names. This constructor is mainly used for testing purposes. + * + * @param descs the array with the descriptors of the available properties + * @param writeMethNames the map with the names of write methods + */ + BeanIntrospectionData(final PropertyDescriptor[] descs, final Map writeMethNames) { + descriptors = descs; + writeMethodNames = writeMethNames; + } + + /** + * Returns the array with property descriptors. + * + * @return the property descriptors for the associated bean class + */ + public PropertyDescriptor[] getDescriptors() { + return descriptors; + } + + /** + * Returns the {@code PropertyDescriptor} for the property with the specified name. If + * this property is unknown, result is null. + * + * @param name the name of the property in question + * @return the {@code PropertyDescriptor} for this property or null + */ + public PropertyDescriptor getDescriptor(final String name) { + PropertyDescriptor[] descriptors = getDescriptors(); + for (int i = 0; i < descriptors.length; ++i) { + PropertyDescriptor pd = descriptors[i]; + if (name.equals(pd.getName())) { + return pd; + } + } + return null; + } + + /** + * Returns the write method for the property determined by the given + * {@code PropertyDescriptor}. This information is normally available in the + * descriptor object itself. However, at least by the ORACLE implementation, the + * method is stored as a {@code SoftReference}. If this reference has been freed by + * the GC, it may be the case that the method cannot be obtained again. Then, + * additional information stored in this object is necessary to obtain the method + * again. + * + * @param beanCls the class of the affected bean + * @param desc the {@code PropertyDescriptor} of the desired property + * @return the write method for this property or null if there is none + */ + public Method getWriteMethod(final Class beanCls, final PropertyDescriptor desc) { + Method method = desc.getWriteMethod(); + if (method == null) { + final String methodName = (String) writeMethodNames.get(desc.getName()); + if (methodName != null) { + method = MethodUtils.getAccessibleMethod(beanCls, methodName, + desc.getPropertyType()); + if (method != null) { + try { + desc.setWriteMethod(method); + } catch (final IntrospectionException e) { + // ignore, in this case the method is not cached + } + } + } + } + + return method; + } + + /** + * Initializes the map with the names of the write methods for the supported + * properties. The method names - if defined - need to be stored separately because + * they may get lost when the GC claims soft references used by the + * {@code PropertyDescriptor} objects. + * + * @param descs the array with the descriptors of the available properties + * @return the map with the names of write methods for properties + */ + private static Map setUpWriteMethodNames(final PropertyDescriptor[] descs) { + final Map methods = new HashMap(); + for (int i = 0; i < descs.length; ++i) { + PropertyDescriptor pd = descs[i]; + final Method method = pd.getWriteMethod(); + if (method != null) { + methods.put(pd.getName(), method.getName()); + } + } + return methods; + } +} diff --git a/src/main/java/org/apache/commons/beanutils/BeanIntrospector.java b/src/main/java/org/apache/commons/beanutils/BeanIntrospector.java new file mode 100644 index 0000000..3f5545e --- /dev/null +++ b/src/main/java/org/apache/commons/beanutils/BeanIntrospector.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.beanutils; + +import java.beans.IntrospectionException; + +/** + *

+ * Definition of an interface for components that can perform introspection on + * bean classes. + *

+ *

+ * Before {@link PropertyUtils} can be used for interaction with a specific Java + * class, the class's properties have to be determined. This is called + * introspection and is initiated automatically on demand. + * PropertyUtils does not perform introspection on its own, but + * delegates this task to one or more objects implementing this interface. This + * makes it possible to customize introspection which may be useful for certain + * code bases using non-standard conventions for accessing properties. + *

+ * + * @version $Id: BeanIntrospector.java 1540359 2013-11-09 18:10:52Z oheger $ + * @since 1.9 + */ +public interface BeanIntrospector { + /** + * Performs introspection on a Java class. The current class to be inspected + * can be queried from the passed in IntrospectionContext + * object. A typical implementation has to obtain this class, determine its + * properties according to the rules it implements, and add them to the + * passed in context object. + * + * @param icontext the context object for interaction with the initiator of + * the introspection request + * @throws IntrospectionException if an error occurs during introspection + */ + void introspect(IntrospectionContext icontext) + throws IntrospectionException; +} diff --git a/src/main/java/org/apache/commons/beanutils/DefaultBeanIntrospector.java b/src/main/java/org/apache/commons/beanutils/DefaultBeanIntrospector.java new file mode 100644 index 0000000..60657d0 --- /dev/null +++ b/src/main/java/org/apache/commons/beanutils/DefaultBeanIntrospector.java @@ -0,0 +1,181 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.beanutils; + +import java.beans.BeanInfo; +import java.beans.IndexedPropertyDescriptor; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + *

+ * The default {@link BeanIntrospector} implementation. + *

+ *

+ * This class implements a default bean introspection algorithm based on the JDK + * classes in the java.beans package. It discovers properties + * conforming to the Java Beans specification. + *

+ *

+ * This class is a singleton. The single instance can be obtained using the + * {@code INSTANCE} field. It does not define any state and thus can be + * shared by arbitrary clients. {@link PropertyUtils} per default uses this + * instance as its only {@code BeanIntrospector} object. + *

+ * + * @version $Id$ + * @since 1.9 + */ +public class DefaultBeanIntrospector implements BeanIntrospector { + /** The singleton instance of this class. */ + public static final BeanIntrospector INSTANCE = new DefaultBeanIntrospector(); + + /** Constant for argument types of a method that expects no arguments. */ + private static final Class[] EMPTY_CLASS_PARAMETERS = new Class[0]; + + /** Constant for arguments types of a method that expects a list argument. */ + private static final Class[] LIST_CLASS_PARAMETER = new Class[] { java.util.List.class }; + + /** Log instance */ + private final Log log = LogFactory.getLog(getClass()); + + /** + * Private constructor so that no instances can be created. + */ + private DefaultBeanIntrospector() { + } + + /** + * Performs introspection of a specific Java class. This implementation uses + * the {@code java.beans.Introspector.getBeanInfo()} method to obtain + * all property descriptors for the current class and adds them to the + * passed in introspection context. + * + * @param icontext the introspection context + */ + public void introspect(final IntrospectionContext icontext) { + BeanInfo beanInfo = null; + try { + beanInfo = Introspector.getBeanInfo(icontext.getTargetClass()); + } catch (final IntrospectionException e) { + // no descriptors are added to the context + log.error( + "Error when inspecting class " + icontext.getTargetClass(), + e); + return; + } + + PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors(); + if (descriptors == null) { + descriptors = new PropertyDescriptor[0]; + } + + handleIndexedPropertyDescriptors(icontext.getTargetClass(), + descriptors); + icontext.addPropertyDescriptors(descriptors); + } + + /** + * This method fixes an issue where IndexedPropertyDescriptor behaves + * differently in different versions of the JDK for 'indexed' properties + * which use java.util.List (rather than an array). It implements a + * workaround for Bug 28358. If you have a Bean with the following + * getters/setters for an indexed property: + * + *
+     * public List getFoo()
+     * public Object getFoo(int index)
+     * public void setFoo(List foo)
+     * public void setFoo(int index, Object foo)
+     * 
+ * + * then the IndexedPropertyDescriptor's getReadMethod() and getWriteMethod() + * behave as follows: + * + * + * @param beanClass the current class to be inspected + * @param descriptors the array with property descriptors + */ + private void handleIndexedPropertyDescriptors(final Class beanClass, + final PropertyDescriptor[] descriptors) { + for (final PropertyDescriptor pd : descriptors) { + if (pd instanceof IndexedPropertyDescriptor) { + final IndexedPropertyDescriptor descriptor = (IndexedPropertyDescriptor) pd; + final String propName = descriptor.getName().substring(0, 1) + .toUpperCase() + + descriptor.getName().substring(1); + + if (descriptor.getReadMethod() == null) { + final String methodName = descriptor.getIndexedReadMethod() != null ? descriptor + .getIndexedReadMethod().getName() : "get" + + propName; + final Method readMethod = MethodUtils + .getMatchingAccessibleMethod(beanClass, methodName, + EMPTY_CLASS_PARAMETERS); + if (readMethod != null) { + try { + descriptor.setReadMethod(readMethod); + } catch (final Exception e) { + log.error( + "Error setting indexed property read method", + e); + } + } + } + if (descriptor.getWriteMethod() == null) { + final String methodName = descriptor.getIndexedWriteMethod() != null ? descriptor + .getIndexedWriteMethod().getName() : "set" + + propName; + Method writeMethod = MethodUtils + .getMatchingAccessibleMethod(beanClass, methodName, + LIST_CLASS_PARAMETER); + if (writeMethod == null) { + for (final Method m : beanClass.getMethods()) { + if (m.getName().equals(methodName)) { + final Class[] parameterTypes = m.getParameterTypes(); + if (parameterTypes.length == 1 + && List.class + .isAssignableFrom(parameterTypes[0])) { + writeMethod = m; + break; + } + } + } + } + if (writeMethod != null) { + try { + descriptor.setWriteMethod(writeMethod); + } catch (final Exception e) { + log.error( + "Error setting indexed property write method", + e); + } + } + } + } + } + } +} diff --git a/src/main/java/org/apache/commons/beanutils/DefaultIntrospectionContext.java b/src/main/java/org/apache/commons/beanutils/DefaultIntrospectionContext.java new file mode 100644 index 0000000..9171198 --- /dev/null +++ b/src/main/java/org/apache/commons/beanutils/DefaultIntrospectionContext.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.beanutils; + +import java.beans.PropertyDescriptor; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + *

+ * An implementation of the {@code IntrospectionContext} interface used by + * {@link PropertyUtilsBean} when doing introspection of a bean class. + *

+ *

+ * This class implements the methods required by the + * {@code IntrospectionContext} interface in a straight-forward manner + * based on a map. It is used internally only. It is not thread-safe. + *

+ * + * @version $Id$ + * @since 1.9 + */ +class DefaultIntrospectionContext implements IntrospectionContext { + /** Constant for an empty array of property descriptors. */ + private static final PropertyDescriptor[] EMPTY_DESCRIPTORS = new PropertyDescriptor[0]; + + /** The current class for introspection. */ + private final Class currentClass; + + /** A map for storing the already added property descriptors. */ + private final Map descriptors; + + /** + * + * Creates a new instance of DefaultIntrospectionContext and sets + * the current class for introspection. + * + * @param cls the current class + */ + public DefaultIntrospectionContext(final Class cls) { + currentClass = cls; + descriptors = new HashMap(); + } + + public Class getTargetClass() { + return currentClass; + } + + public void addPropertyDescriptor(final PropertyDescriptor desc) { + if (desc == null) { + throw new IllegalArgumentException( + "Property descriptor must not be null!"); + } + descriptors.put(desc.getName(), desc); + } + + public void addPropertyDescriptors(final PropertyDescriptor[] descs) { + if (descs == null) { + throw new IllegalArgumentException( + "Array with descriptors must not be null!"); + } + + for (int i = 0; i < descs.length; ++i) { + addPropertyDescriptor(descs[i]); + } + } + + public boolean hasProperty(final String name) { + return descriptors.containsKey(name); + } + + public PropertyDescriptor getPropertyDescriptor(final String name) { + return (PropertyDescriptor) descriptors.get(name); + } + + public void removePropertyDescriptor(final String name) { + descriptors.remove(name); + } + + public Set propertyNames() { + return descriptors.keySet(); + } + + /** + * Returns an array with all descriptors added to this context. This method + * is used to obtain the results of introspection. + * + * @return an array with all known property descriptors + */ + public PropertyDescriptor[] getPropertyDescriptors() { + return (PropertyDescriptor[]) descriptors.values().toArray(EMPTY_DESCRIPTORS); + } +} diff --git a/src/main/java/org/apache/commons/beanutils/IntrospectionContext.java b/src/main/java/org/apache/commons/beanutils/IntrospectionContext.java new file mode 100644 index 0000000..c48f186 --- /dev/null +++ b/src/main/java/org/apache/commons/beanutils/IntrospectionContext.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.beanutils; + +import java.beans.PropertyDescriptor; +import java.util.Set; + +/** + *

+ * A context interface used during introspection for querying and setting + * property descriptors. + *

+ *

+ * An implementation of this interface is passed to {@link BeanIntrospector} + * objects during processing of a bean class. It allows the + * {@code BeanIntrospector} to deliver descriptors for properties it has + * detected. It is also possible to find out which properties have already been + * found by another {@code BeanIntrospector}; this allows multiple + * {@code BeanIntrospector} instances to collaborate. + *

+ * + * @version $Id: IntrospectionContext.java 1540359 2013-11-09 18:10:52Z oheger $ + * @since 1.9 + */ +public interface IntrospectionContext { + /** + * Returns the class that is subject of introspection. + * + * @return the current class + */ + Class getTargetClass(); + + /** + * Adds the given property descriptor to this context. This method is called + * by a {@code BeanIntrospector} during introspection for each detected + * property. If this context already contains a descriptor for the affected + * property, it is overridden. + * + * @param desc the property descriptor + */ + void addPropertyDescriptor(PropertyDescriptor desc); + + /** + * Adds an array of property descriptors to this context. Using this method + * multiple descriptors can be added at once. + * + * @param descriptors the array of descriptors to be added + */ + void addPropertyDescriptors(PropertyDescriptor[] descriptors); + + /** + * Tests whether a descriptor for the property with the given name is + * already contained in this context. This method can be used for instance + * to prevent that an already existing property descriptor is overridden. + * + * @param name the name of the property in question + * @return true if a descriptor for this property has already been + * added, false otherwise + */ + boolean hasProperty(String name); + + /** + * Returns the descriptor for the property with the given name or + * null if this property is unknown. + * + * @param name the name of the property in question + * @return the descriptor for this property or null if this property + * is unknown + */ + PropertyDescriptor getPropertyDescriptor(String name); + + /** + * Removes the descriptor for the property with the given name. + * + * @param name the name of the affected property + */ + void removePropertyDescriptor(String name); + + /** + * Returns a set with the names of all properties known to this context. + * + * @return a set with the known property names + */ + Set propertyNames(); +} diff --git a/src/main/java/org/apache/commons/beanutils/PropertyUtils.java b/src/main/java/org/apache/commons/beanutils/PropertyUtils.java index 6344b4f..b3ce0e6 100644 --- a/src/main/java/org/apache/commons/beanutils/PropertyUtils.java +++ b/src/main/java/org/apache/commons/beanutils/PropertyUtils.java @@ -890,5 +890,20 @@ public class PropertyUtils { PropertyUtilsBean.getInstance().setSimpleProperty(bean, name, value); } + public static void addBeanIntrospector(final BeanIntrospector introspector) { + PropertyUtilsBean.getInstance().addBeanIntrospector(introspector); + } + /** + * Removes the specified BeanIntrospector. + * + * @param introspector the BeanIntrospector to be removed + * @return true if the BeanIntrospector existed and + * could be removed, false otherwise + * @since 1.9 + */ + public static boolean removeBeanIntrospector(final BeanIntrospector introspector) { + return PropertyUtilsBean.getInstance().removeBeanIntrospector( + introspector); + } } diff --git a/src/main/java/org/apache/commons/beanutils/PropertyUtilsBean.java b/src/main/java/org/apache/commons/beanutils/PropertyUtilsBean.java index 1894f21..c7817cd 100644 --- a/src/main/java/org/apache/commons/beanutils/PropertyUtilsBean.java +++ b/src/main/java/org/apache/commons/beanutils/PropertyUtilsBean.java @@ -37,6 +37,7 @@ import org.apache.commons.collections.FastHashMap; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import java.util.concurrent.CopyOnWriteArrayList; /** * Utility methods for using Java Reflection APIs to facilitate generic @@ -130,7 +131,9 @@ public class PropertyUtilsBean { /** Log instance */ private Log log = LogFactory.getLog(PropertyUtils.class); - + + /** The list with BeanIntrospector objects. */ + private final List introspectors; // ---------------------------------------------------------- Constructors /** Base constructor */ @@ -139,6 +142,8 @@ public class PropertyUtilsBean { descriptorsCache.setFast(true); mappedDescriptorsCache = new WeakFastHashMap(); mappedDescriptorsCache.setFast(true); + introspectors = new CopyOnWriteArrayList(); + resetBeanIntrospectors(); } @@ -183,6 +188,47 @@ public class PropertyUtilsBean { } } + /** + * Resets the {@link BeanIntrospector} objects registered at this instance. After this + * method was called, only the default {@code BeanIntrospector} is registered. + * + * @since 1.9 + */ + public final void resetBeanIntrospectors() { + introspectors.clear(); + introspectors.add(DefaultBeanIntrospector.INSTANCE); + introspectors.add(SuppressPropertiesBeanIntrospector.SUPPRESS_CLASS); + } + + /** + * Adds a BeanIntrospector. This object is invoked when the + * property descriptors of a class need to be obtained. + * + * @param introspector the BeanIntrospector to be added (must + * not be null + * @throws IllegalArgumentException if the argument is null + * @since 1.9 + */ + public void addBeanIntrospector(final BeanIntrospector introspector) { + if (introspector == null) { + throw new IllegalArgumentException( + "BeanIntrospector must not be null!"); + } + introspectors.add(introspector); + } + + /** + * Removes the specified BeanIntrospector. + * + * @param introspector the BeanIntrospector to be removed + * @return true if the BeanIntrospector existed and + * could be removed, false otherwise + * @since 1.9 + */ + public boolean removeBeanIntrospector(final BeanIntrospector introspector) { + return introspectors.remove(introspector); + } + /** * Clear any cached property descriptors information for all classes * loaded by any class loaders. This is useful in cases where class @@ -909,17 +955,12 @@ public class PropertyUtilsBean { return (null); } - PropertyDescriptor[] descriptors = getPropertyDescriptors(bean); - if (descriptors != null) { - - for (int i = 0; i < descriptors.length; i++) { - if (name.equals(descriptors[i].getName())) { - return (descriptors[i]); - } - } + final BeanIntrospectionData data = getIntrospectionData(bean.getClass()); + PropertyDescriptor result = data.getDescriptor(name); + if (result != null) { + return result; } - PropertyDescriptor result = null; FastHashMap mappedDescriptors = getMappedPropertyDescriptors(bean); if (mappedDescriptors == null) { @@ -960,110 +1001,10 @@ public class PropertyUtilsBean { * @exception IllegalArgumentException if beanClass is null */ public PropertyDescriptor[] - getPropertyDescriptors(Class beanClass) { - - if (beanClass == null) { - throw new IllegalArgumentException("No bean class specified"); - } - - // Look up any cached descriptors for this bean class - PropertyDescriptor[] descriptors = null; - descriptors = - (PropertyDescriptor[]) descriptorsCache.get(beanClass); - if (descriptors != null) { - return (descriptors); - } - - // Introspect the bean and cache the generated descriptors - BeanInfo beanInfo = null; - try { - beanInfo = Introspector.getBeanInfo(beanClass); - } catch (IntrospectionException e) { - return (new PropertyDescriptor[0]); - } - descriptors = beanInfo.getPropertyDescriptors(); - if (descriptors == null) { - descriptors = new PropertyDescriptor[0]; - } - - // ----------------- Workaround for Bug 28358 --------- START ------------------ - // - // The following code fixes an issue where IndexedPropertyDescriptor behaves - // Differently in different versions of the JDK for 'indexed' properties which - // use java.util.List (rather than an array). - // - // If you have a Bean with the following getters/setters for an indexed property: - // - // public List getFoo() - // public Object getFoo(int index) - // public void setFoo(List foo) - // public void setFoo(int index, Object foo) - // - // then the IndexedPropertyDescriptor's getReadMethod() and getWriteMethod() - // behave as follows: - // - // JDK 1.3.1_04: returns valid Method objects from these methods. - // JDK 1.4.2_05: returns null from these methods. - // - for (int i = 0; i < descriptors.length; i++) { - if (descriptors[i] instanceof IndexedPropertyDescriptor) { - IndexedPropertyDescriptor descriptor = (IndexedPropertyDescriptor)descriptors[i]; - String propName = descriptor.getName().substring(0, 1).toUpperCase() + - descriptor.getName().substring(1); - - if (descriptor.getReadMethod() == null) { - String methodName = descriptor.getIndexedReadMethod() != null - ? descriptor.getIndexedReadMethod().getName() - : "get" + propName; - Method readMethod = MethodUtils.getMatchingAccessibleMethod(beanClass, - methodName, - EMPTY_CLASS_PARAMETERS); - if (readMethod != null) { - try { - descriptor.setReadMethod(readMethod); - } catch(Exception e) { - log.error("Error setting indexed property read method", e); - } - } - } - if (descriptor.getWriteMethod() == null) { - String methodName = descriptor.getIndexedWriteMethod() != null - ? descriptor.getIndexedWriteMethod().getName() - : "set" + propName; - Method writeMethod = MethodUtils.getMatchingAccessibleMethod(beanClass, - methodName, - LIST_CLASS_PARAMETER); - if (writeMethod == null) { - Method[] methods = beanClass.getMethods(); - for (int j = 0; j < methods.length; j++) { - if (methods[j].getName().equals(methodName)) { - Class[] parameterTypes = methods[j].getParameterTypes(); - if (parameterTypes.length == 1 && - List.class.isAssignableFrom(parameterTypes[0])) { - writeMethod = methods[j]; - break; - } - } - } - } - if (writeMethod != null) { - try { - descriptor.setWriteMethod(writeMethod); - } catch(Exception e) { - log.error("Error setting indexed property write method", e); - } - } - } - } - } - // ----------------- Workaround for Bug 28358 ---------- END ------------------- - - descriptorsCache.put(beanClass, descriptors); - return (descriptors); - + getPropertyDescriptors(Class beanClass) { + return getIntrospectionData(beanClass).getDescriptors(); } - /** *

Retrieve the property descriptors for the specified bean, * introspecting and caching them the first time a particular bean class @@ -2248,4 +2189,52 @@ public class PropertyUtilsBean { } } + + /** + * Obtains the {@code BeanIntrospectionData} object describing the specified bean + * class. This object is looked up in the internal cache. If necessary, introspection + * is performed now on the affected bean class, and the results object is created. + * + * @param beanClass the bean class in question + * @return the {@code BeanIntrospectionData} object for this class + * @throws IllegalArgumentException if the bean class is null + */ + private BeanIntrospectionData getIntrospectionData(final Class beanClass) { + if (beanClass == null) { + throw new IllegalArgumentException("No bean class specified"); + } + + // Look up any cached information for this bean class + BeanIntrospectionData data = (BeanIntrospectionData) descriptorsCache.get(beanClass); + if (data == null) { + data = fetchIntrospectionData(beanClass); + descriptorsCache.put(beanClass, data); + } + + return data; + } + + /** + * Performs introspection on the specified class. This method invokes all {@code BeanIntrospector} objects that were + * added to this instance. + * + * @param beanClass the class to be inspected + * @return a data object with the results of introspection + */ + private BeanIntrospectionData fetchIntrospectionData(final Class beanClass) { + final DefaultIntrospectionContext ictx = new DefaultIntrospectionContext(beanClass); + + Iterator it = introspectors.iterator(); + while (it.hasNext()) { + final BeanIntrospector bi = (BeanIntrospector) it.next(); + + try { + bi.introspect(ictx); + } catch (final IntrospectionException iex) { + log.error("Exception during introspection", iex); + } + } + + return new BeanIntrospectionData(ictx.getPropertyDescriptors()); + } } diff --git a/src/main/java/org/apache/commons/beanutils/SuppressPropertiesBeanIntrospector.java b/src/main/java/org/apache/commons/beanutils/SuppressPropertiesBeanIntrospector.java new file mode 100644 index 0000000..9d4e3b3 --- /dev/null +++ b/src/main/java/org/apache/commons/beanutils/SuppressPropertiesBeanIntrospector.java @@ -0,0 +1,75 @@ +package org.apache.commons.beanutils; + +import java.beans.IntrospectionException; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + *

+ * A specialized {@code BeanIntrospector} implementation which suppresses some properties. + *

+ *

+ * An instance of this class is passed a set with the names of the properties it should + * process. During introspection of a bean class it removes all these properties from the + * {@link IntrospectionContext}. So effectively, properties added by a different + * {@code BeanIntrospector} are removed again. + *

+ * + * @version $Id$ + * @since 1.9.2 + */ +public class SuppressPropertiesBeanIntrospector implements BeanIntrospector { + /** + * A specialized instance which is configured to suppress the special {@code class} + * properties of Java beans. Unintended access to the property {@code class} (which is + * common to all Java objects) can be a security risk because it also allows access to + * the class loader. Adding this instance as {@code BeanIntrospector} to an instance + * of {@code PropertyUtilsBean} suppresses the {@code class} property; it can then no + * longer be accessed. + */ + public static final SuppressPropertiesBeanIntrospector SUPPRESS_CLASS = + new SuppressPropertiesBeanIntrospector(Collections.singleton("class")); + + /** A set with the names of the properties to be suppressed. */ + private final Set propertyNames; + + /** + * Creates a new instance of {@code SuppressPropertiesBeanIntrospector} and sets the + * names of the properties to be suppressed. + * + * @param propertiesToSuppress the names of the properties to be suppressed (must not + * be null) + * @throws IllegalArgumentException if the collection with property names is + * null + */ + public SuppressPropertiesBeanIntrospector(Collection propertiesToSuppress) { + if (propertiesToSuppress == null) { + throw new IllegalArgumentException("Property names must not be null!"); + } + + propertyNames = Collections.unmodifiableSet(new HashSet( + propertiesToSuppress)); + } + + /** + * Returns a (unmodifiable) set with the names of the properties which are suppressed + * by this {@code BeanIntrospector}. + * + * @return a set with the names of the suppressed properties + */ + public Set getSuppressedProperties() { + return propertyNames; + } + + /** + * {@inheritDoc} This implementation removes all properties from the given context it + * is configured for. + */ + public void introspect(IntrospectionContext icontext) throws IntrospectionException { + for (String property : getSuppressedProperties()) { + icontext.removePropertyDescriptor(property); + } + } +} diff --git a/src/test/java/org/apache/commons/beanutils/SuppressPropertiesBeanIntrospectorTestCase.java b/src/test/java/org/apache/commons/beanutils/SuppressPropertiesBeanIntrospectorTestCase.java new file mode 100644 index 0000000..ed93e70 --- /dev/null +++ b/src/test/java/org/apache/commons/beanutils/SuppressPropertiesBeanIntrospectorTestCase.java @@ -0,0 +1,127 @@ +package org.apache.commons.beanutils; + +import java.beans.IntrospectionException; +import java.beans.PropertyDescriptor; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import junit.framework.TestCase; + +/** + * Test class for {@code SuppressPropertiesBeanIntrospector}. + * + * @version $Id$ + */ +public class SuppressPropertiesBeanIntrospectorTestCase extends TestCase { + /** + * Tries to create an instance without properties. + */ + public void testInitNoPropertyNames() { + try { + new SuppressPropertiesBeanIntrospector(null); + fail("Missing properties not detected!"); + } catch (IllegalArgumentException iaex) { + // ok + } + } + + /** + * Tests whether the expected properties have been removed during introspection. + */ + public void testRemovePropertiesDuringIntrospection() throws IntrospectionException { + String[] properties = { "test", "other", "oneMore" }; + SuppressPropertiesBeanIntrospector introspector = new SuppressPropertiesBeanIntrospector( + Arrays.asList(properties)); + IntrospectionContextTestImpl context = new IntrospectionContextTestImpl(); + + introspector.introspect(context); + assertEquals("Wrong number of removed properties", properties.length, context + .getRemovedProperties().size()); + for (String property : properties) { + assertTrue("Property not removed: " + property, context + .getRemovedProperties().contains(property)); + } + } + + /** + * Tests that a defensive copy is created from the collection with properties to be + * removed. + */ + public void testPropertyNamesDefensiveCopy() throws IntrospectionException { + Collection properties = new HashSet(); + properties.add("prop1"); + SuppressPropertiesBeanIntrospector introspector = new SuppressPropertiesBeanIntrospector( + properties); + properties.add("prop2"); + IntrospectionContextTestImpl context = new IntrospectionContextTestImpl(); + + introspector.introspect(context); + assertEquals("Wrong number of removed properties", 1, context + .getRemovedProperties().size()); + assertTrue("Wrong removed property", + context.getRemovedProperties().contains("prop1")); + } + + /** + * Tests that the set with properties to be removed cannot be modified. + */ + public void testGetSuppressedPropertiesModify() { + SuppressPropertiesBeanIntrospector introspector = new SuppressPropertiesBeanIntrospector( + Arrays.asList("p1", "p2")); + Set properties = introspector.getSuppressedProperties(); + try { + properties.add("anotherProperty"); + fail("Could modify properties"); + } catch (UnsupportedOperationException uoex) { + // ok + } + } + + /** + * A test implementation of IntrospectionContext which collects the properties which + * have been removed. + */ + private static class IntrospectionContextTestImpl implements IntrospectionContext { + /** Stores the names of properties which have been removed. */ + private final Set removedProperties = new HashSet(); + + /** + * Returns the names of properties which have been removed. + * + * @return the set with removed properties + */ + public Set getRemovedProperties() { + return removedProperties; + } + + public Class getTargetClass() { + throw new UnsupportedOperationException("Unexpected method call!"); + } + + public void addPropertyDescriptor(PropertyDescriptor desc) { + throw new UnsupportedOperationException("Unexpected method call!"); + } + + public void addPropertyDescriptors(PropertyDescriptor[] descriptors) { + throw new UnsupportedOperationException("Unexpected method call!"); + } + + public boolean hasProperty(String name) { + throw new UnsupportedOperationException("Unexpected method call!"); + } + + public PropertyDescriptor getPropertyDescriptor(String name) { + throw new UnsupportedOperationException("Unexpected method call!"); + } + + public void removePropertyDescriptor(String name) { + removedProperties.add(name); + } + + public Set propertyNames() { + throw new UnsupportedOperationException("Unexpected method call!"); + } + } +} diff --git a/src/test/java/org/apache/commons/beanutils/bugs/Jira463TestCase.java b/src/test/java/org/apache/commons/beanutils/bugs/Jira463TestCase.java new file mode 100644 index 0000000..31ac31c --- /dev/null +++ b/src/test/java/org/apache/commons/beanutils/bugs/Jira463TestCase.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.beanutils.bugs; + +import junit.framework.TestCase; + +import org.apache.commons.beanutils.AlphaBean; +import org.apache.commons.beanutils.BeanUtilsBean; +import org.apache.commons.beanutils.SuppressPropertiesBeanIntrospector; + +/** + * Class loader vulnerability in DefaultResolver + * + * @version $Id$ + * @see https://issues.apache.org/jira/browse/BEANUTILS-463 + */ +public class Jira463TestCase extends TestCase { + /** + * Tests that with a specialized {@code BeanIntrospector} implementation the class + * property can be suppressed. + */ + public void testSuppressClassProperty() throws Exception { + BeanUtilsBean bub = new BeanUtilsBean(); + bub.getPropertyUtils().addBeanIntrospector( + SuppressPropertiesBeanIntrospector.SUPPRESS_CLASS); + AlphaBean bean = new AlphaBean(); + try { + bub.getProperty(bean, "class"); + fail("Could access class property!"); + } catch (NoSuchMethodException ex) { + // ok + } + } +} diff --git a/src/test/java/org/apache/commons/beanutils/bugs/Jira520TestCase.java b/src/test/java/org/apache/commons/beanutils/bugs/Jira520TestCase.java new file mode 100644 index 0000000..2a70811 --- /dev/null +++ b/src/test/java/org/apache/commons/beanutils/bugs/Jira520TestCase.java @@ -0,0 +1,39 @@ +package org.apache.commons.beanutils.bugs; + +import org.apache.commons.beanutils.AlphaBean; +import org.apache.commons.beanutils.BeanUtilsBean; +import org.apache.commons.beanutils.SuppressPropertiesBeanIntrospector; + +import junit.framework.TestCase; + +/** + * Fix CVE: https://nvd.nist.gov/vuln/detail/CVE-2014-0114 + * + * @see https://issues.apache.org/jira/browse/BEANUTILS-520 + */ +public class Jira520TestCase extends TestCase { + /** + * By default opt-in to security that does not allow access to "class". + */ + public void testSuppressClassPropertyByDefault() throws Exception { + final BeanUtilsBean bub = new BeanUtilsBean(); + final AlphaBean bean = new AlphaBean(); + try { + bub.getProperty(bean, "class"); + fail("Could access class property!"); + } catch (final NoSuchMethodException ex) { + // ok + } + } + + /** + * Allow opt-out to make your app less secure but allow access to "class". + */ + public void testAllowAccessToClassProperty() throws Exception { + final BeanUtilsBean bub = new BeanUtilsBean(); + bub.getPropertyUtils().removeBeanIntrospector(SuppressPropertiesBeanIntrospector.SUPPRESS_CLASS); + final AlphaBean bean = new AlphaBean(); + String result = bub.getProperty(bean, "class"); + assertEquals("Class property should have been accessed", "class org.apache.commons.beanutils.AlphaBean", result); + } +} \ No newline at end of file -- 2.21.0