/* Copyright 2007 Ben Gunter * * Licensed 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 net.sourceforge.stripes.controller; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpSession; import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.action.ActionBeanContext; import net.sourceforge.stripes.action.StrictBinding; import net.sourceforge.stripes.action.StrictBinding.Policy; import net.sourceforge.stripes.exception.StripesRuntimeException; import net.sourceforge.stripes.util.Log; import net.sourceforge.stripes.util.bean.NodeEvaluation; import net.sourceforge.stripes.util.bean.PropertyExpressionEvaluation; import net.sourceforge.stripes.validation.ValidationMetadata; import net.sourceforge.stripes.validation.ValidationMetadataProvider; /** * Manages the policies observed by {@link DefaultActionBeanPropertyBinder} when binding properties * to an {@link ActionBean}. * * @author Ben Gunter * @see StrictBinding */ @StrictBinding(defaultPolicy = Policy.ALLOW) public class BindingPolicyManager { /** List of classes that, for security reasons, are not allowed as a {@link NodeEvaluation} value type. */ private static final List<Class<?>> ILLEGAL_NODE_VALUE_TYPES = Arrays.<Class<?>> asList( ActionBeanContext.class, Class.class, ClassLoader.class, HttpSession.class, ServletRequest.class, ServletResponse.class); /** The regular expression that a property name must match */ private static final String PROPERTY_REGEX = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*"; /** The compiled form of {@link #PROPERTY_REGEX} */ private static final Pattern PROPERTY_PATTERN = Pattern.compile(PROPERTY_REGEX); /** Log */ private static final Log log = Log.getInstance(BindingPolicyManager.class); /** Cached instances */ private static final Map<Class<?>, BindingPolicyManager> instances = new ConcurrentHashMap<Class<?>, BindingPolicyManager>(); /** * Get the policy manager for the given class. Instances are cached and returned on subsequent * calls. * * @param beanType the class whose policy manager is to be retrieved * @return a policy manager */ public static BindingPolicyManager getInstance(Class<?> beanType) { BindingPolicyManager oldInstance = instances.get(beanType); if(oldInstance != null) { return oldInstance; } else { BindingPolicyManager newInstance = new BindingPolicyManager(beanType); oldInstance = instances.put(beanType, newInstance); if(oldInstance != null) { newInstance = oldInstance; } return newInstance; } } /** The class to which the binding policy applies */ private Class<?> beanClass; /** The default policy to honor, in case of conflicts */ private Policy defaultPolicy; /** The regular expression that allowed properties must match */ private Pattern allowPattern; /** The regular expression that denied properties must match */ private Pattern denyPattern; /** The regular expression that matches properties with {@literal @Validate} */ private Pattern validatePattern; /** * Create a new instance to handle binding security for the given type. * * @param beanClass the class to which the binding policy applies */ protected BindingPolicyManager(Class<?> beanClass) { try { log.debug("Creating ", getClass().getName(), " for ", beanClass, " with default policy ", defaultPolicy); this.beanClass = beanClass; // process the annotation StrictBinding annotation = getAnnotation(beanClass); if (annotation != null) { // set default policy this.defaultPolicy = annotation.defaultPolicy(); // construct the allow pattern this.allowPattern = globToPattern(annotation.allow()); // construct the deny pattern this.denyPattern = globToPattern(annotation.deny()); // construct the validated properties pattern this.validatePattern = globToPattern(getValidatedProperties(beanClass)); } } catch (Exception e) { log.error(e, "%%% Failure instantiating ", getClass().getName()); StripesRuntimeException sre = new StripesRuntimeException(e.getMessage(), e); sre.setStackTrace(e.getStackTrace()); throw sre; } } /** * Indicates if binding is allowed for the given expression. * * @param eval a property expression that has been evaluated against an {@link ActionBean} * @return true if binding is allowed; false if not */ public boolean isBindingAllowed(PropertyExpressionEvaluation eval) { // Ensure no-one is trying to bind into a protected type if (usesIllegalNodeValueType(eval)) { return false; } // check parameter name against access lists String paramName = new ParameterName(eval.getExpression().getSource()).getStrippedName(); boolean deny = denyPattern != null && denyPattern.matcher(paramName).matches(); boolean allow = (allowPattern != null && allowPattern.matcher(paramName).matches()) || (validatePattern != null && validatePattern.matcher(paramName).matches()); /* * if path appears on neither or both lists ( i.e. !(allow ^ deny) ) and default policy is * to deny access, then fail */ if (defaultPolicy == Policy.DENY && !(allow ^ deny)) return false; /* * regardless of default policy, if it's in the deny list but not in the allow list, then * fail */ if (!allow && deny) return false; // any other conditions pass the test return true; } /** * Indicates if any node in the given {@link PropertyExpressionEvaluation} has a value type that is assignable from * any of the classes listed in {@link #ILLEGAL_NODE_VALUE_TYPES}. * * @param eval a property expression that has been evaluated against an {@link ActionBean} * @return true if the expression uses an illegal node value type; false otherwise */ protected boolean usesIllegalNodeValueType(PropertyExpressionEvaluation eval) { for (NodeEvaluation node = eval.getRootNode(); node != null; node = node.getNext()) { Type type = node.getValueType(); if (type instanceof ParameterizedType) { type = ((ParameterizedType) type).getRawType(); } if (type instanceof Class) { final Class<?> nodeClass = (Class<?>) type; for (Class<?> protectedClass : ILLEGAL_NODE_VALUE_TYPES) { if (protectedClass.isAssignableFrom(nodeClass)) { return true; } } } } return false; } /** * Get the {@link StrictBinding} annotation for a class, checking all its superclasses if * necessary. If no annotation is found, then one will be returned whose default policy is to * allow binding to all properties. * * @param beanType the class to get the {@link StrictBinding} annotation for * @return An annotation. This method never returns null. */ protected StrictBinding getAnnotation(Class<?> beanType) { StrictBinding annotation; do { annotation = beanType.getAnnotation(StrictBinding.class); } while (annotation == null && (beanType = beanType.getSuperclass()) != null); if (annotation == null) { annotation = getClass().getAnnotation(StrictBinding.class); } return annotation; } /** * Get all the properties and nested properties of the given class for which there is a * corresponding {@link ValidationMetadata}, as returned by * {@link ValidationMetadataProvider#getValidationMetadata(Class, ParameterName)}. The idea * here is that if the bean property must be validated, then it is expected that the property * may be bound to the bean. * * @param beanClass a class * @return The validated properties. If no properties are annotated then null. * @see ValidationMetadataProvider#getValidationMetadata(Class) */ protected String[] getValidatedProperties(Class<?> beanClass) { Set<String> properties = StripesFilter.getConfiguration().getValidationMetadataProvider() .getValidationMetadata(beanClass).keySet(); return new ArrayList<String>(properties).toArray(new String[properties.size()]); } /** * Get the bean class. * * @return the bean class */ public Class<?> getBeanClass() { return beanClass; } /** * Get the default policy. * * @return the policy */ public Policy getDefaultPolicy() { return defaultPolicy; } /** * Converts a glob to a regex {@link Pattern}. * * @param globArray an array of property name globs, each of which may be a comma separated list * of globs * @return the pattern */ protected Pattern globToPattern(String... globArray) { if (globArray == null || globArray.length == 0) return null; // things are much easier if we convert to a single list List<String> globs = new ArrayList<String>(); for (String glob : globArray) { String[] subs = glob.split("(\\s*,\\s*)+"); for (String sub : subs) { globs.add(sub); } } List<String> subs = new ArrayList<String>(); StringBuilder buf = new StringBuilder(); for (String glob : globs) { buf.setLength(0); String[] properties = glob.split("\\."); for (int i = 0; i < properties.length; i++) { String property = properties[i]; if ("*".equals(property)) { buf.append(PROPERTY_REGEX); } else if ("**".equals(property)) { buf.append(PROPERTY_REGEX).append("(\\.").append(PROPERTY_REGEX).append(")*"); } else if (property.length() > 0) { Matcher matcher = PROPERTY_PATTERN.matcher(property); if (matcher.matches()) { buf.append(property); } else { log.warn("Invalid property name: " + property); return null; } } // add a literal dot after all but the last if (i < properties.length - 1) buf.append("\\."); } // add to the list of subs if (buf.length() != 0) subs.add(buf.toString()); } // join subs together with pipes and compile buf.setLength(0); for (String sub : subs) { buf.append(sub).append('|'); } if (buf.length() > 0) buf.setLength(buf.length() - 1); log.debug("Translated globs ", Arrays.toString(globArray), " to regex ", buf); // return null if pattern is empty if (buf.length() == 0) return null; else return Pattern.compile(buf.toString()); } }