ParametersInterceptor.java
/*
* 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.struts2.interceptor.parameter;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.ActionContext;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.ModelDriven;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.action.NoParameters;
import org.apache.struts2.action.ParameterNameAware;
import org.apache.struts2.action.ParameterValueAware;
import org.apache.struts2.dispatcher.HttpParameters;
import org.apache.struts2.dispatcher.Parameter;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.interceptor.MethodFilterInterceptor;
import org.apache.struts2.ognl.OgnlUtil;
import org.apache.struts2.ognl.ThreadAllowlist;
import org.apache.struts2.security.AcceptedPatternsChecker;
import org.apache.struts2.security.DefaultAcceptedPatternsChecker;
import org.apache.struts2.security.ExcludedPatternsChecker;
import org.apache.struts2.util.ClearableValueStack;
import org.apache.struts2.util.MemberAccessValueStack;
import org.apache.struts2.util.ProxyUtil;
import org.apache.struts2.util.TextParseUtil;
import org.apache.struts2.util.ValueStack;
import org.apache.struts2.util.ValueStackFactory;
import org.apache.struts2.util.reflection.ReflectionContextState;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Pattern;
import static java.lang.String.format;
import static java.util.Collections.unmodifiableSet;
import static java.util.stream.Collectors.joining;
import static org.apache.commons.lang3.StringUtils.indexOfAny;
import static org.apache.commons.lang3.StringUtils.normalizeSpace;
import static org.apache.struts2.security.DefaultAcceptedPatternsChecker.NESTING_CHARS;
import static org.apache.struts2.security.DefaultAcceptedPatternsChecker.NESTING_CHARS_STR;
import static org.apache.struts2.util.DebugUtils.logWarningForFirstOccurrence;
import static org.apache.struts2.util.DebugUtils.notifyDeveloperOfError;
/**
* This interceptor sets all parameters on the value stack.
*/
public class ParametersInterceptor extends MethodFilterInterceptor {
private static final Logger LOG = LogManager.getLogger(ParametersInterceptor.class);
protected static final int PARAM_NAME_MAX_LENGTH = 100;
private static final Pattern DMI_IGNORED_PATTERN = Pattern.compile("^(action|method):.*", Pattern.CASE_INSENSITIVE);
private int paramNameMaxLength = PARAM_NAME_MAX_LENGTH;
private boolean devMode = false;
private boolean dmiEnabled = false;
protected boolean ordered = false;
protected boolean requireAnnotations = false;
protected boolean requireAnnotationsTransitionMode = false;
private ValueStackFactory valueStackFactory;
private OgnlUtil ognlUtil;
protected ThreadAllowlist threadAllowlist;
private ExcludedPatternsChecker excludedPatterns;
private AcceptedPatternsChecker acceptedPatterns;
private Set<Pattern> excludedValuePatterns = null;
private Set<Pattern> acceptedValuePatterns = null;
@Inject
public void setValueStackFactory(ValueStackFactory valueStackFactory) {
this.valueStackFactory = valueStackFactory;
}
@Inject
public void setOgnlUtil(OgnlUtil ognlUtil) {
this.ognlUtil = ognlUtil;
}
@Inject
public void setThreadAllowlist(ThreadAllowlist threadAllowlist) {
this.threadAllowlist = threadAllowlist;
}
@Inject(StrutsConstants.STRUTS_DEVMODE)
public void setDevMode(String mode) {
this.devMode = BooleanUtils.toBoolean(mode);
}
@Inject(value = StrutsConstants.STRUTS_PARAMETERS_REQUIRE_ANNOTATIONS, required = false)
public void setRequireAnnotations(String requireAnnotations) {
this.requireAnnotations = BooleanUtils.toBoolean(requireAnnotations);
if (!this.requireAnnotations) {
String msg = "@StrutsParameter annotation requirement is disabled!" +
" We strongly recommend keeping it enabled to protect against critical vulnerabilities." +
" Set the configuration `{}=true` to enable it." +
" Please refer to the Struts 7.0 migration guide and security documentation for further information.";
logWarningForFirstOccurrence("strutsParameter", LOG, msg, StrutsConstants.STRUTS_PARAMETERS_REQUIRE_ANNOTATIONS);
}
}
/**
* When 'Transition Mode' is enabled, parameters that are not 'nested' will be accepted without annotations. What
* this means in practice is that all public setters on an Action will be exposed for parameter injection again, and
* only 'nested' parameters, i.e. public getters on an Action, will require annotations.
* <p>
* In this mode, the OGNL auto-allowlisting capability is not degraded in any way, and as such, it offers a
* convenient option for applications to enable the OGNL allowlist capability whilst they work through the process
* of annotating all their Action parameters.
*/
@Inject(value = StrutsConstants.STRUTS_PARAMETERS_REQUIRE_ANNOTATIONS_TRANSITION, required = false)
public void setRequireAnnotationsTransitionMode(String transitionMode) {
this.requireAnnotationsTransitionMode = BooleanUtils.toBoolean(transitionMode);
}
@Inject
public void setExcludedPatterns(ExcludedPatternsChecker excludedPatterns) {
this.excludedPatterns = excludedPatterns;
}
@Inject
public void setAcceptedPatterns(AcceptedPatternsChecker acceptedPatterns) {
this.acceptedPatterns = acceptedPatterns;
}
@Inject(value = StrutsConstants.STRUTS_ENABLE_DYNAMIC_METHOD_INVOCATION, required = false)
protected void setDynamicMethodInvocation(String dmiEnabled) {
this.dmiEnabled = Boolean.parseBoolean(dmiEnabled);
}
/**
* If the param name exceeds the configured maximum length it will not be
* accepted.
*
* @param paramNameMaxLength Maximum length of param names
*/
public void setParamNameMaxLength(int paramNameMaxLength) {
this.paramNameMaxLength = paramNameMaxLength;
}
static private int countOGNLCharacters(String s) {
int count = 0;
for (int i = s.length() - 1; i >= 0; i--) {
char c = s.charAt(i);
if (c == '.' || c == '[') count++;
}
return count;
}
/**
* Compares based on number of '.' and '[' characters (fewer is higher)
*/
static final Comparator<String> rbCollator = (s1, s2) -> {
int l1 = countOGNLCharacters(s1);
int l2 = countOGNLCharacters(s2);
return l1 < l2 ? -1 : (l2 < l1 ? 1 : s1.compareTo(s2));
};
@Override
public String doIntercept(ActionInvocation invocation) throws Exception {
Object action = invocation.getAction();
if (action instanceof NoParameters) {
return invocation.invoke();
}
ActionContext actionContext = invocation.getInvocationContext();
HttpParameters parameters = retrieveParameters(actionContext);
if (parameters == null) {
return invocation.invoke();
}
if (LOG.isDebugEnabled()) {
LOG.debug("Setting params {}", normalizeSpace(getParameterLogMap(parameters)));
}
Map<String, Object> contextMap = actionContext.getContextMap();
batchApplyReflectionContextState(contextMap, true);
try {
applyParameters(action, actionContext.getValueStack(), parameters);
} finally {
batchApplyReflectionContextState(contextMap, false);
}
return invocation.invoke();
}
/**
* Gets the parameter map to apply from wherever appropriate
*
* @param actionContext The action context
* @return The parameter map to apply
*/
protected HttpParameters retrieveParameters(ActionContext actionContext) {
return actionContext.getParameters();
}
/**
* Adds the parameters into context's ParameterMap
* <p>
* In this class this is a no-op, since the parameters were fetched from the same location. In subclasses both this
* and {@link #retrieveParameters} should be overridden.
*
* @param ac The action context
* @param newParams The parameter map to apply
*/
protected void addParametersToContext(ActionContext ac, Map<String, ?> newParams) {
}
protected void applyParameters(final Object action, ValueStack stack, HttpParameters parameters) {
Map<String, Parameter> acceptableParameters = toAcceptableParameters(parameters, action);
ValueStack newStack = toNewStack(stack);
batchApplyReflectionContextState(newStack.getContext(), true);
applyMemberAccessProperties(newStack);
applyParametersOnStack(newStack, acceptableParameters, action);
if (newStack instanceof ClearableValueStack) {
stack.getActionContext().withConversionErrors(newStack.getActionContext().getConversionErrors());
}
addParametersToContext(ActionContext.getContext(), acceptableParameters);
}
protected void batchApplyReflectionContextState(Map<String, Object> context, boolean value) {
ReflectionContextState.setCreatingNullObjects(context, value);
ReflectionContextState.setDenyMethodExecution(context, value);
ReflectionContextState.setReportingConversionErrors(context, value);
}
protected ValueStack toNewStack(ValueStack stack) {
ValueStack newStack = valueStackFactory.createValueStack(stack);
if (newStack instanceof ClearableValueStack clearable) {
clearable.clearContextValues();
newStack.getActionContext().withLocale(stack.getActionContext().getLocale()).withValueStack(stack);
}
return newStack;
}
protected void applyMemberAccessProperties(ValueStack stack) {
if (!(stack instanceof MemberAccessValueStack accessValueStack)) {
return;
}
accessValueStack.useAcceptProperties(acceptedPatterns.getAcceptedPatterns());
accessValueStack.useExcludeProperties(excludedPatterns.getExcludedPatterns());
}
protected Map<String, Parameter> toAcceptableParameters(HttpParameters parameters, Object action) {
HttpParameters newParams = initNewHttpParameters(parameters);
Map<String, Parameter> acceptableParameters = initParameterMap();
for (Map.Entry<String, Parameter> entry : newParams.entrySet()) {
String parameterName = entry.getKey();
Parameter parameterValue = entry.getValue();
if (isAcceptableParameter(parameterName, action) && isAcceptableParameterValue(parameterValue, action)) {
acceptableParameters.put(parameterName, parameterValue);
}
}
return acceptableParameters;
}
protected Map<String, Parameter> initParameterMap() {
if (ordered) {
return new TreeMap<>(getOrderedComparator());
} else {
return new TreeMap<>();
}
}
protected HttpParameters initNewHttpParameters(HttpParameters parameters) {
if (ordered) {
return HttpParameters.create().withComparator(getOrderedComparator()).withParent(parameters).build();
} else {
return HttpParameters.create().withParent(parameters).build();
}
}
protected void applyParametersOnStack(ValueStack stack, Map<String, Parameter> parameters, Object action) {
for (Map.Entry<String, Parameter> entry : parameters.entrySet()) {
try {
stack.setParameter(entry.getKey(), entry.getValue().getObject());
} catch (RuntimeException e) {
if (devMode) {
notifyDeveloperParameterException(action, entry.getKey(), e.getMessage());
}
}
}
}
protected void notifyDeveloperParameterException(Object action, String property, String message) {
String logMsg = format("Unexpected Exception caught setting '%s' on '%s: %s", property, action.getClass(), message);
notifyDeveloperOfError(LOG, action, logMsg);
}
/**
* Checks if name of parameter can be accepted or thrown away
*
* @param name parameter name
* @param action current action
* @return true if parameter is accepted
*/
protected boolean isAcceptableParameter(String name, Object action) {
return isAcceptableName(name) && isAcceptableParameterNameAware(name, action) && isParameterAnnotatedAndAllowlist(name, action);
}
protected boolean isAcceptableParameterNameAware(String name, Object action) {
return !(action instanceof ParameterNameAware nameAware) || nameAware.acceptableParameterName(name);
}
/**
* Checks if the Action class member corresponding to a parameter is appropriately annotated with
* {@link StrutsParameter} and OGNL allowlists any necessary classes.
* <p>
* Note that this logic relies on the use of {@link DefaultAcceptedPatternsChecker#NESTING_CHARS} and may also
* be adversely impacted by the use of custom OGNL property accessors.
*/
protected boolean isParameterAnnotatedAndAllowlist(String name, Object action) {
if (!requireAnnotations) {
return true;
}
long paramDepth = name.codePoints().mapToObj(c -> (char) c).filter(NESTING_CHARS::contains).count();
if (action instanceof ModelDriven<?> && !ActionContext.getContext().getValueStack().peek().equals(action)) {
LOG.debug("Model driven Action detected, exempting from @StrutsParameter annotation requirement");
return true;
}
if (requireAnnotationsTransitionMode && paramDepth == 0) {
LOG.debug("Annotation transition mode enabled, exempting non-nested parameter [{}] from @StrutsParameter annotation requirement", name);
return true;
}
int nestingIndex = indexOfAny(name, NESTING_CHARS_STR);
String rootProperty = nestingIndex == -1 ? name : name.substring(0, nestingIndex);
String normalisedRootProperty = Character.toLowerCase(rootProperty.charAt(0)) + rootProperty.substring(1);
return hasValidAnnotatedMember(normalisedRootProperty, action, paramDepth);
}
/**
* Note that we check for a public field last or only if there is no valid, annotated property descriptor. This is
* because this check is likely to fail more often than not, as the relative use of public fields is low - so we
* save computation by checking this last.
*/
protected boolean hasValidAnnotatedMember(String rootProperty, Object action, long paramDepth) {
LOG.debug("Checking Action [{}] for a matching, correctly annotated member for property [{}]",
action.getClass().getSimpleName(), rootProperty);
BeanInfo beanInfo = getBeanInfo(action);
if (beanInfo == null) {
return hasValidAnnotatedField(action, rootProperty, paramDepth);
}
Optional<PropertyDescriptor> propDescOpt = Arrays.stream(beanInfo.getPropertyDescriptors())
.filter(desc -> desc.getName().equals(rootProperty)).findFirst();
if (propDescOpt.isEmpty()) {
return hasValidAnnotatedField(action, rootProperty, paramDepth);
}
if (hasValidAnnotatedPropertyDescriptor(action, propDescOpt.get(), paramDepth)) {
return true;
}
return hasValidAnnotatedField(action, rootProperty, paramDepth);
}
protected boolean hasValidAnnotatedPropertyDescriptor(Object action, PropertyDescriptor propDesc, long paramDepth) {
Class<?> actionClass = ultimateClass(action);
Method relevantMethod = paramDepth == 0 ? propDesc.getWriteMethod() : propDesc.getReadMethod();
if (relevantMethod == null) {
return false;
}
if (getPermittedInjectionDepth(relevantMethod) < paramDepth) {
String logMessage = format(
"Parameter injection for method [%s] on Action [%s] rejected. Ensure it is annotated with @StrutsParameter with an appropriate 'depth'.",
relevantMethod.getName(),
relevantMethod.getDeclaringClass().getName());
if (devMode) {
notifyDeveloperOfError(LOG, action, logMessage);
} else {
LOG.debug(logMessage);
}
return false;
}
LOG.debug("Success: Matching annotated method [{}] found for property [{}] of depth [{}] on Action [{}]",
relevantMethod.getName(), propDesc.getName(), paramDepth, actionClass.getSimpleName());
if (paramDepth >= 1) {
allowlistClass(propDesc.getPropertyType());
}
if (paramDepth >= 2) {
allowlistReturnTypeIfParameterized(relevantMethod);
}
return true;
}
protected void allowlistReturnTypeIfParameterized(Method method) {
allowlistParameterizedTypeArg(method.getGenericReturnType());
}
protected void allowlistParameterizedTypeArg(Type genericType) {
if (!(genericType instanceof ParameterizedType)) {
return;
}
Type[] paramTypes = ((ParameterizedType) genericType).getActualTypeArguments();
allowlistParamType(paramTypes[0]);
if (paramTypes.length > 1) {
// Probably useful for Map or Map-like classes
allowlistParamType(paramTypes[1]);
}
}
protected void allowlistParamType(Type paramType) {
if (paramType instanceof Class<?> clazz) {
allowlistClass(clazz);
}
}
protected void allowlistClass(Class<?> clazz) {
threadAllowlist.allowClassHierarchy(clazz);
}
protected boolean hasValidAnnotatedField(Object action, String fieldName, long paramDepth) {
Class<?> actionClass = ultimateClass(action);
LOG.debug("No matching annotated method found for property [{}] of depth [{}] on Action [{}], now also checking for public field",
fieldName, paramDepth, actionClass.getSimpleName());
Field field;
try {
field = actionClass.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
LOG.debug("Matching field for property [{}] not found on Action [{}]", fieldName, actionClass.getSimpleName());
return false;
}
if (!Modifier.isPublic(field.getModifiers())) {
LOG.debug("Matching field [{}] is not public on Action [{}]", field.getName(), actionClass.getSimpleName());
return false;
}
if (getPermittedInjectionDepth(field) < paramDepth) {
String logMessage = format(
"Parameter injection for field [%s] on Action [%s] rejected. Ensure it is annotated with @StrutsParameter with an appropriate 'depth'.",
field.getName(),
actionClass.getName());
if (devMode) {
notifyDeveloperOfError(LOG, action, logMessage);
} else {
LOG.debug(logMessage);
}
return false;
}
LOG.debug("Success: Matching annotated public field [{}] found for property of depth [{}] on Action [{}]",
field.getName(), paramDepth, actionClass.getSimpleName());
if (paramDepth >= 1) {
allowlistClass(field.getType());
}
if (paramDepth >= 2) {
allowlistFieldIfParameterized(field);
}
return true;
}
protected void allowlistFieldIfParameterized(Field field) {
allowlistParameterizedTypeArg(field.getGenericType());
}
/**
* @return permitted injection depth where -1 indicates not permitted
*/
protected int getPermittedInjectionDepth(AnnotatedElement element) {
StrutsParameter annotation = getParameterAnnotation(element);
if (annotation == null) {
return -1;
}
return annotation.depth();
}
/**
* Annotation retrieval logic. Can be overridden to support extending annotations or some other form of annotation
* inheritance.
*/
protected StrutsParameter getParameterAnnotation(AnnotatedElement element) {
return element.getAnnotation(StrutsParameter.class);
}
protected Class<?> ultimateClass(Object action) {
if (ProxyUtil.isProxy(action)) {
return ProxyUtil.ultimateTargetClass(action);
}
return action.getClass();
}
protected BeanInfo getBeanInfo(Object action) {
Class<?> actionClass = ultimateClass(action);
try {
return ognlUtil.getBeanInfo(actionClass);
} catch (IntrospectionException e) {
LOG.warn("Error introspecting Action {} for parameter injection validation", actionClass, e);
return null;
}
}
/**
* Checks if parameter value can be accepted or thrown away
*
* @param param the parameter
* @param action current action
* @return true if parameter is accepted
*/
protected boolean isAcceptableParameterValue(Parameter param, Object action) {
return isAcceptableParameterValueAware(param, action) && isAcceptableValue(param.getName(), param.getValue());
}
protected boolean isAcceptableParameterValueAware(Parameter param, Object action) {
return !(action instanceof ParameterValueAware valueAware) || valueAware.acceptableParameterValue(param.getValue());
}
/**
* Gets an instance of the comparator to use for the ordered sorting. Override this
* method to customize the ordering of the parameters as they are set to the
* action.
*
* @return A comparator to sort the parameters
*/
protected Comparator<String> getOrderedComparator() {
return rbCollator;
}
protected String getParameterLogMap(HttpParameters parameters) {
if (parameters == null) {
return "NONE";
}
return parameters.entrySet().stream()
.map(entry -> format("%s => %s ", entry.getKey(), entry.getValue().getValue()))
.collect(joining());
}
/**
* Validates the name passed is:
* * Within the max length of a parameter name
* * Is not excluded
* * Is accepted
*
* @param name - Name to check
* @return true if accepted
*/
protected boolean isAcceptableName(String name) {
if (isIgnoredDMI(name)) {
LOG.trace("DMI is enabled, ignoring DMI method: {}", name);
return false;
}
boolean accepted = isWithinLengthLimit(name) && !isExcluded(name) && isAccepted(name);
if (devMode && accepted) {
LOG.debug("Parameter [{}] was accepted and will be appended to action!", name);
}
return accepted;
}
private boolean isIgnoredDMI(String name) {
if (!dmiEnabled) {
return false;
}
return DMI_IGNORED_PATTERN.matcher(name).matches();
}
/**
* Validates:
* * Value is null/blank
* * Value is not excluded
* * Value is accepted
*
* @param name - Param name (for logging)
* @param value - value to check
* @return true if accepted
*/
protected boolean isAcceptableValue(String name, String value) {
boolean accepted = value == null || value.isEmpty() || (!isParamValueExcluded(value) && isParamValueAccepted(value));
if (!accepted) {
String message = "Value [{}] of parameter [{}] was not accepted and will be dropped!";
if (devMode) {
LOG.warn(message, normalizeSpace(value), normalizeSpace(name));
} else {
LOG.debug(message, normalizeSpace(value), normalizeSpace(name));
}
}
return accepted;
}
protected boolean isWithinLengthLimit(String name) {
boolean matchLength = name.length() <= paramNameMaxLength;
if (!matchLength) {
if (devMode) {
LOG.warn("Parameter [{}] is too long, allowed length is [{}]. Use Interceptor Parameter Overriding " +
"to override the limit, see more at\n" +
"https://struts.apache.org/core-developers/interceptors.html#interceptor-parameter-overriding",
name, paramNameMaxLength);
} else {
LOG.warn("Parameter [{}] is too long, allowed length is [{}]", name, paramNameMaxLength);
}
}
return matchLength;
}
protected boolean isAccepted(String paramName) {
AcceptedPatternsChecker.IsAccepted result = acceptedPatterns.isAccepted(paramName);
if (!result.isAccepted()) {
if (devMode) {
LOG.warn("Parameter [{}] didn't match accepted pattern [{}]! See Accepted / Excluded patterns at\n" +
"https://struts.apache.org/security/#accepted--excluded-patterns",
paramName, result.getAcceptedPattern());
} else {
LOG.debug("Parameter [{}] didn't match accepted pattern [{}]!", paramName, result.getAcceptedPattern());
}
return false;
}
return true;
}
protected boolean isExcluded(String paramName) {
ExcludedPatternsChecker.IsExcluded result = excludedPatterns.isExcluded(paramName);
if (result.isExcluded()) {
if (devMode) {
LOG.warn("Parameter [{}] matches excluded pattern [{}]! See Accepted / Excluded patterns at\n" +
"https://struts.apache.org/security/#accepted--excluded-patterns",
paramName, result.getExcludedPattern());
} else {
LOG.debug("Parameter [{}] matches excluded pattern [{}]!", paramName, result.getExcludedPattern());
}
return true;
}
return false;
}
protected boolean isParamValueExcluded(String value) {
if (!hasParamValuesToExclude()) {
LOG.debug("'excludedValuePatterns' not defined so anything is allowed");
return false;
}
for (Pattern excludedValuePattern : excludedValuePatterns) {
if (excludedValuePattern.matcher(value).matches()) {
if (devMode) {
LOG.warn("Parameter value [{}] matches excluded pattern [{}]! See Accepting/Excluding parameter values at\n" +
"https://struts.apache.org/core-developers/parameters-interceptor#excluding-parameter-values",
value, excludedValuePatterns);
} else {
LOG.debug("Parameter value [{}] matches excluded pattern [{}]", value, excludedValuePattern);
}
return true;
}
}
return false;
}
protected boolean isParamValueAccepted(String value) {
if (!hasParamValuesToAccept()) {
LOG.debug("'acceptedValuePatterns' not defined so anything is allowed");
return true;
}
for (Pattern acceptedValuePattern : acceptedValuePatterns) {
if (acceptedValuePattern.matcher(value).matches()) {
return true;
}
}
if (devMode) {
LOG.warn("Parameter value [{}] didn't match accepted pattern [{}]! See Accepting/Excluding parameter values at\n" +
"https://struts.apache.org/core-developers/parameters-interceptor#excluding-parameter-values",
value, acceptedValuePatterns);
} else {
LOG.debug("Parameter value [{}] was not accepted!", value);
}
return false;
}
private boolean hasParamValuesToExclude() {
return excludedValuePatterns != null && !excludedValuePatterns.isEmpty();
}
private boolean hasParamValuesToAccept() {
return acceptedValuePatterns != null && !acceptedValuePatterns.isEmpty();
}
/**
* Whether to order the parameters or not
*
* @return True to order
*/
public boolean isOrdered() {
return ordered;
}
/**
* Set whether to order the parameters by object depth or not
*
* @param ordered True to order them
*/
public void setOrdered(boolean ordered) {
this.ordered = ordered;
}
/**
* Sets a comma-delimited list of regular expressions to match
* parameters that are allowed in the parameter map (aka whitelist).
* <p>
* Don't change the default unless you know what you are doing in terms
* of security implications.
* </p>
*
* @param commaDelim A comma-delimited list of regular expressions
*/
public void setAcceptParamNames(String commaDelim) {
acceptedPatterns.setAcceptedPatterns(commaDelim);
}
/**
* Sets a comma-delimited list of regular expressions to match
* parameters that should be removed from the parameter map.
*
* @param commaDelim A comma-delimited list of regular expressions
*/
public void setExcludeParams(String commaDelim) {
excludedPatterns.setExcludedPatterns(commaDelim);
}
/**
* Sets a comma-delimited list of regular expressions to match
* values of parameters that should be accepted and included in the parameter map.
*
* @param commaDelimitedPatterns A comma-delimited set of regular expressions
*/
public void setAcceptedValuePatterns(String commaDelimitedPatterns) {
Set<String> patterns = TextParseUtil.commaDelimitedStringToSet(commaDelimitedPatterns);
if (acceptedValuePatterns == null) {
// Limit unwanted log entries (for 1st call, acceptedValuePatterns null)
LOG.debug("Sets accepted value patterns to [{}], note this may impact the safety of your application!", patterns);
} else {
LOG.warn("Replacing accepted patterns [{}] with [{}], be aware that this may impact safety of your application!",
acceptedValuePatterns, patterns);
}
acceptedValuePatterns = new HashSet<>(patterns.size());
try {
for (String pattern : patterns) {
acceptedValuePatterns.add(Pattern.compile(pattern, Pattern.CASE_INSENSITIVE));
}
} finally {
acceptedValuePatterns = unmodifiableSet(acceptedValuePatterns);
}
}
/**
* Sets a comma-delimited list of regular expressions to match
* values of parameters that should be removed from the parameter map.
*
* @param commaDelimitedPatterns A comma-delimited set of regular expressions
*/
public void setExcludedValuePatterns(String commaDelimitedPatterns) {
Set<String> patterns = TextParseUtil.commaDelimitedStringToSet(commaDelimitedPatterns);
if (excludedValuePatterns == null) {
// Limit unwanted log entries (for 1st call, excludedValuePatterns null)
LOG.debug("Setting excluded value patterns to [{}]", patterns);
} else {
LOG.warn("Replacing excluded value patterns [{}] with [{}], be aware that this may impact safety of your application!",
excludedValuePatterns, patterns);
}
excludedValuePatterns = new HashSet<>(patterns.size());
try {
for (String pattern : patterns) {
excludedValuePatterns.add(Pattern.compile(pattern, Pattern.CASE_INSENSITIVE));
}
} finally {
excludedValuePatterns = unmodifiableSet(excludedValuePatterns);
}
}
}