Skip to content

Commit

Permalink
Merge pull request #969 from apache/WW-5429-param-anno-log
Browse files Browse the repository at this point in the history
WW-5429 Log parameter annotation issues at ERROR level when in DevMode
  • Loading branch information
kusalk committed Jun 21, 2024
2 parents a895450 + ba46c18 commit 898a8d9
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@
/**
* ValidationAware classes can accept Action (class level) or field level error messages. Action level messages are kept
* in a Collection. Field level error messages are kept in a Map from String field name to a List of field error msgs.
*
* @author plightbo
*/
public interface ValidationAware {

Expand Down Expand Up @@ -119,7 +117,9 @@ public interface ValidationAware {
*
* @return <code>(hasActionErrors() || hasFieldErrors())</code>
*/
boolean hasErrors();
default boolean hasErrors() {
return hasActionErrors() || hasFieldErrors();
}

/**
* Check whether there are any field errors associated with this action.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ private ErrorMessageBuilder() {
}

public ErrorMessageBuilder errorSettingExpressionWithValue(String expr, Object value) {
appenExpression(expr);
appendExpression(expr);
if (value instanceof Object[]) {
appendValueAsArray((Object[]) value, message);
} else {
Expand All @@ -42,7 +42,7 @@ public ErrorMessageBuilder errorSettingExpressionWithValue(String expr, Object v
return this;
}

private void appenExpression(String expr) {
private void appendExpression(String expr) {
message.append("Error setting expression '");
message.append(expr);
message.append("' with value ");
Expand Down
42 changes: 42 additions & 0 deletions core/src/main/java/com/opensymphony/xwork2/util/DebugUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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 com.opensymphony.xwork2.util;

import com.opensymphony.xwork2.TextProvider;
import com.opensymphony.xwork2.interceptor.ValidationAware;
import org.apache.logging.log4j.Logger;

/**
* @since 6.5.0
*/
public final class DebugUtils {

public static void notifyDeveloperOfError(Logger log, Object action, String message) {
if (action instanceof TextProvider) {
TextProvider tp = (TextProvider) action;
message = tp.getText("devmode.notification", "Developer Notification:\n{0}", new String[]{message});
}
log.error(message);
if (action instanceof ValidationAware) {
ValidationAware validationAware = (ValidationAware) action;
validationAware.addActionError(message);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,8 @@

import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.TextProvider;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.interceptor.MethodFilterInterceptor;
import com.opensymphony.xwork2.interceptor.ValidationAware;
import com.opensymphony.xwork2.security.AcceptedPatternsChecker;
import com.opensymphony.xwork2.security.DefaultAcceptedPatternsChecker;
import com.opensymphony.xwork2.security.ExcludedPatternsChecker;
Expand Down Expand Up @@ -56,7 +54,6 @@
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Map;
Expand All @@ -67,6 +64,8 @@

import static com.opensymphony.xwork2.security.DefaultAcceptedPatternsChecker.NESTING_CHARS;
import static com.opensymphony.xwork2.security.DefaultAcceptedPatternsChecker.NESTING_CHARS_STR;
import static com.opensymphony.xwork2.util.DebugUtils.notifyDeveloperOfError;
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;
Expand Down Expand Up @@ -317,19 +316,8 @@ protected void applyParametersOnStack(ValueStack stack, Map<String, Parameter> p
}

protected void notifyDeveloperParameterException(Object action, String property, String message) {
String logMsg = "Unexpected Exception caught setting '" + property + "' on '" + action.getClass() + ": " + message;
if (action instanceof TextProvider) {
TextProvider tp = (TextProvider) action;
logMsg = tp.getText("devmode.notification", "Developer Notification:\n{0}", new String[]{logMsg});
}
LOG.error(logMsg);

if (action instanceof ValidationAware) {
ValidationAware validationAware = (ValidationAware) action;
Collection<String> messages = validationAware.getActionMessages();
messages.add(message);
validationAware.setActionMessages(messages);
}
String logMsg = format("Unexpected Exception caught setting '%s' on '%s: %s", property, action.getClass(), message);
notifyDeveloperOfError(LOG, action, logMsg);
}

/**
Expand Down Expand Up @@ -388,23 +376,37 @@ protected boolean hasValidAnnotatedMember(String rootProperty, Object action, lo
return hasValidAnnotatedField(action, rootProperty, paramDepth);
}

if (hasValidAnnotatedPropertyDescriptor(propDescOpt.get(), paramDepth)) {
if (hasValidAnnotatedPropertyDescriptor(action, propDescOpt.get(), paramDepth)) {
return true;
}

return hasValidAnnotatedField(action, rootProperty, paramDepth);
}

/**
* @deprecated since 6.5.0, use {@link #hasValidAnnotatedPropertyDescriptor(Object, PropertyDescriptor, long)}
* instead.
*/
@Deprecated
protected boolean hasValidAnnotatedPropertyDescriptor(PropertyDescriptor propDesc, long paramDepth) {
return hasValidAnnotatedPropertyDescriptor(null, propDesc, paramDepth);
}

protected boolean hasValidAnnotatedPropertyDescriptor(Object action, PropertyDescriptor propDesc, long paramDepth) {
Method relevantMethod = paramDepth == 0 ? propDesc.getWriteMethod() : propDesc.getReadMethod();
if (relevantMethod == null) {
return false;
}
if (getPermittedInjectionDepth(relevantMethod) < paramDepth) {
LOG.debug(
"Parameter injection for method [{}] on action [{}] rejected. Ensure it is annotated with @StrutsParameter with an appropriate 'depth'.",
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;
}
if (paramDepth >= 1) {
Expand Down Expand Up @@ -455,10 +457,15 @@ protected boolean hasValidAnnotatedField(Object action, String fieldName, long p
return false;
}
if (getPermittedInjectionDepth(field) < paramDepth) {
LOG.debug(
"Parameter injection for field [{}] on action [{}] rejected. Ensure it is annotated with @StrutsParameter with an appropriate 'depth'.",
String logMessage = format(
"Parameter injection for field [%s] on action [%s] rejected. Ensure it is annotated with @StrutsParameter with an appropriate 'depth'.",
fieldName,
action.getClass().getName());
if (devMode) {
notifyDeveloperOfError(LOG, action, logMessage);
} else {
LOG.debug(logMessage);
}
return false;
}
if (paramDepth >= 1) {
Expand Down Expand Up @@ -533,7 +540,7 @@ protected String getParameterLogMap(HttpParameters parameters) {
return "NONE";
}
return parameters.entrySet().stream()
.map(entry -> String.format("%s => %s ", entry.getKey(), entry.getValue().getValue()))
.map(entry -> format("%s => %s ", entry.getKey(), entry.getValue().getValue()))
.collect(joining());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,17 @@ public void testInsecureParameters() throws Exception {
pi.setParameters(action, vs, HttpParameters.create(params).build());

// then
assertEquals(3, action.getActionMessages().size());
assertEquals(3, action.getActionErrors().size());

String msg1 = action.getActionMessage(0);
String msg2 = action.getActionMessage(1);
String msg3 = action.getActionMessage(2);
List<String> actionErrors = new ArrayList<>(action.getActionErrors());

assertEquals("Error setting expression 'expression' with value '#f=#_memberAccess.getClass().getDeclaredField('allowStaticMethodAccess'),#f.setAccessible(true),#f.set(#_memberAccess,true),#req=@org.apache.struts2.ServletActionContext@getRequest(),#resp=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),#resp.println(#req.getRealPath('/')),#resp.close()'", msg1);
assertEquals("Error setting expression 'name' with value '(#context[\"xwork.MethodAccessor.denyMethodExecution\"]= new java.lang.Boolean(false), #_memberAccess[\"allowStaticMethodAccess\"]= new java.lang.Boolean(true), @java.lang.Runtime@getRuntime().exec('mkdir /tmp/PWNAGE'))(meh)'", msg2);
assertEquals("Error setting expression 'top['name'](0)' with value 'true'", msg3);
String msg1 = actionErrors.get(0);
String msg2 = actionErrors.get(1);
String msg3 = actionErrors.get(2);

assertEquals("Unexpected Exception caught setting 'expression' on 'class org.apache.struts2.interceptor.parameter.ValidateAction: Error setting expression 'expression' with value '#f=#_memberAccess.getClass().getDeclaredField('allowStaticMethodAccess'),#f.setAccessible(true),#f.set(#_memberAccess,true),#req=@org.apache.struts2.ServletActionContext@getRequest(),#resp=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),#resp.println(#req.getRealPath('/')),#resp.close()'", msg1);
assertEquals("Unexpected Exception caught setting 'name' on 'class org.apache.struts2.interceptor.parameter.ValidateAction: Error setting expression 'name' with value '(#context[\"xwork.MethodAccessor.denyMethodExecution\"]= new java.lang.Boolean(false), #_memberAccess[\"allowStaticMethodAccess\"]= new java.lang.Boolean(true), @java.lang.Runtime@getRuntime().exec('mkdir /tmp/PWNAGE'))(meh)'", msg2);
assertEquals("Unexpected Exception caught setting 'top['name'](0)' on 'class org.apache.struts2.interceptor.parameter.ValidateAction: Error setting expression 'top['name'](0)' with value 'true'", msg3);
assertNull(action.getName());
}

Expand Down Expand Up @@ -201,15 +203,16 @@ protected boolean isExcluded(String paramName) {
pi.setParameters(action, vs, HttpParameters.create(params).build());

// then
assertEquals(3, action.getActionMessages().size());
assertEquals(3, action.getActionErrors().size());

String msg1 = action.getActionMessage(0);
String msg2 = action.getActionMessage(1);
String msg3 = action.getActionMessage(2);
List<String> actionErrors = new ArrayList<>(action.getActionErrors());
String msg1 = actionErrors.get(0);
String msg2 = actionErrors.get(1);
String msg3 = actionErrors.get(2);

assertEquals("Error setting expression 'class.classLoader.defaultAssertionStatus' with value 'true'", msg1);
assertEquals("Error setting expression 'class.classLoader.jarPath' with value 'bad'", msg2);
assertEquals("Error setting expression 'model.class.classLoader.jarPath' with value 'very bad'", msg3);
assertEquals("Unexpected Exception caught setting 'class.classLoader.defaultAssertionStatus' on 'class org.apache.struts2.interceptor.parameter.ValidateAction: Error setting expression 'class.classLoader.defaultAssertionStatus' with value 'true'", msg1);
assertEquals("Unexpected Exception caught setting 'class.classLoader.jarPath' on 'class org.apache.struts2.interceptor.parameter.ValidateAction: Error setting expression 'class.classLoader.jarPath' with value 'bad'", msg2);
assertEquals("Unexpected Exception caught setting 'model.class.classLoader.jarPath' on 'class org.apache.struts2.interceptor.parameter.ValidateAction: Error setting expression 'model.class.classLoader.jarPath' with value 'very bad'", msg3);

assertFalse(excluded.get(pollution1));
assertFalse(excluded.get(pollution2));
Expand Down Expand Up @@ -582,8 +585,8 @@ public void testNonexistentParametersGetLoggedInDevMode() throws Exception {
container.inject(config.getInterceptors().get(0).getInterceptor());
ActionProxy proxy = actionProxyFactory.createActionProxy("", MockConfigurationProvider.PARAM_INTERCEPTOR_ACTION_NAME, null, extraContext.getContextMap());
proxy.execute();
final String actionMessage = "" + ((SimpleAction) proxy.getAction()).getActionMessages().toArray()[0];
assertTrue(actionMessage.contains("Error setting expression 'not_a_property' with value 'There is no action property named like this'"));
final String actionError = "" + ((SimpleAction) proxy.getAction()).getActionErrors().toArray()[0];
assertTrue(actionError.contains("Error setting expression 'not_a_property' with value 'There is no action property named like this'"));
}

public void testNonexistentParametersAreIgnoredInProductionMode() throws Exception {
Expand Down Expand Up @@ -1014,59 +1017,65 @@ protected void setUp() throws Exception {
class ValidateAction implements ValidationAware {

private final List<String> messages = new LinkedList<>();
private final List<String> errors = new LinkedList<>();
private String name;

@Override
public void setActionErrors(Collection<String> errorMessages) {
}

@Override
public Collection<String> getActionErrors() {
return null;
return errors;
}

@Override
public void setActionMessages(Collection<String> messages) {
}

@Override
public Collection<String> getActionMessages() {
return messages;
}

@Override
public void setFieldErrors(Map<String, List<String>> errorMap) {
}

@Override
public Map<String, List<String>> getFieldErrors() {
return null;
}

@Override
public void addActionError(String anErrorMessage) {
errors.add(anErrorMessage);
}

@Override
public void addActionMessage(String aMessage) {
messages.add(aMessage);
}

@Override
public void addFieldError(String fieldName, String errorMessage) {
}

@Override
public boolean hasActionErrors() {
return false;
return !errors.isEmpty();
}

@Override
public boolean hasActionMessages() {
return !messages.isEmpty();
}

public boolean hasErrors() {
return false;
}

@Override
public boolean hasFieldErrors() {
return false;
}

public String getActionMessage(int index) {
return messages.get(index);
}

public String getName() {
return name;
}
Expand Down

0 comments on commit 898a8d9

Please sign in to comment.