This is an automated email from the ASF dual-hosted git repository.
lukaszlenart pushed a commit to branch support/struts-6-x-x
in repository https://gitbox.apache.org/repos/asf/struts.git
The following commit(s) were added to refs/heads/support/struts-6-x-x by this
push:
new 84ef60eae WW-5535 fix(core): enforce class-level HTTP method
annotations for wildcard-resolved unannotated methods (#1693)
84ef60eae is described below
commit 84ef60eae824194587719f0acca120c879e22c6f
Author: Lukasz Lenart <[email protected]>
AuthorDate: Mon May 25 16:38:04 2026 +0200
WW-5535 fix(core): enforce class-level HTTP method annotations for
wildcard-resolved unannotated methods (#1693)
The WW-5535 change to DefaultActionProxy.resolveMethod() (which made
wildcard-resolved methods report isMethodSpecified()=true) interacted
with HttpMethodInterceptor's if/else-if so that the class-level
annotation branch became unreachable when the resolved method carried
no method-level annotation:
if (isMethodSpecified()) {
if (method has annotation) return doIntercept(method);
// unannotated method falls through silently
} else if (class has annotation) {
return doIntercept(class); // never reached when
methodSpecified=true
}
Convert the else-if to a standalone if so the class-level check is
always evaluated as a fallback. Method-level annotations still take
precedence — they are checked first and return early.
Adds three tests:
- testWildcardResolvedUnannotatedMethodRespectsClassLevelAnnotation:
GET on a wildcard-resolved unannotated method is rejected when the
class is @AllowedHttpMethod(POST).
- testWildcardResolvedUnannotatedMethodAllowsPostWithClassLevelAnnotation:
POST on the same configuration succeeds.
- testWildcardResolvedExecuteRejectsGetThroughRealProxy: end-to-end
via a real DefaultActionProxy with <action name="Wild-*" method="{1}">,
resolving to ActionSupport.execute().
---
.../httpmethod/HttpMethodInterceptor.java | 3 +-
.../httpmethod/HttpMethodInterceptorTest.java | 71 ++++++++++++++++++++++
2 files changed, 73 insertions(+), 1 deletion(-)
diff --git
a/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpMethodInterceptor.java
b/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpMethodInterceptor.java
index c0afca1c3..4d033d338 100644
---
a/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpMethodInterceptor.java
+++
b/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpMethodInterceptor.java
@@ -90,7 +90,8 @@ public class HttpMethodInterceptor extends
AbstractInterceptor {
invocation.getProxy().getMethod(),
AllowedHttpMethod.class.getSimpleName(), request.getMethod());
return doIntercept(invocation, method);
}
- } else if (AnnotationUtils.isAnnotatedBy(action.getClass(),
HTTP_METHOD_ANNOTATIONS)) {
+ }
+ if (AnnotationUtils.isAnnotatedBy(action.getClass(),
HTTP_METHOD_ANNOTATIONS)) {
LOG.debug("Action: {} annotated with: {}, checking if request: {}
meets allowed methods!",
action, AllowedHttpMethod.class.getSimpleName(),
request.getMethod());
return doIntercept(invocation, action.getClass());
diff --git
a/core/src/test/java/org/apache/struts2/interceptor/httpmethod/HttpMethodInterceptorTest.java
b/core/src/test/java/org/apache/struts2/interceptor/httpmethod/HttpMethodInterceptorTest.java
index f68b01df3..edbf56554 100644
---
a/core/src/test/java/org/apache/struts2/interceptor/httpmethod/HttpMethodInterceptorTest.java
+++
b/core/src/test/java/org/apache/struts2/interceptor/httpmethod/HttpMethodInterceptorTest.java
@@ -19,13 +19,17 @@
package org.apache.struts2.interceptor.httpmethod;
import com.opensymphony.xwork2.ActionContext;
+import com.opensymphony.xwork2.ActionProxy;
import com.opensymphony.xwork2.mock.MockActionInvocation;
import com.opensymphony.xwork2.mock.MockActionProxy;
import org.apache.struts2.HttpMethodsTestAction;
import org.apache.struts2.StrutsInternalTestCase;
import org.apache.struts2.TestAction;
+import org.apache.struts2.config.StrutsXmlConfigurationProvider;
import org.springframework.mock.web.MockHttpServletRequest;
+import java.util.Map;
+
public class HttpMethodInterceptorTest extends StrutsInternalTestCase {
private HttpMethodInterceptor interceptor;
@@ -254,6 +258,73 @@ public class HttpMethodInterceptorTest extends
StrutsInternalTestCase {
assertEquals(HttpMethod.POST, action.getHttpMethod());
}
+ /**
+ * Regression for wildcard-resolved methods with no method-level HTTP
annotation:
+ * a class-level {@code @AllowedHttpMethod(POST)} must still cause GET to
be rejected.
+ * Previously the interceptor's {@code if/else-if} structure made the
class-level
+ * branch unreachable when {@code isMethodSpecified()=true} and the
resolved method
+ * carried no annotation of its own.
+ */
+ public void
testWildcardResolvedUnannotatedMethodRespectsClassLevelAnnotation() throws
Exception {
+ HttpMethodsTestAction action = new HttpMethodsTestAction();
+ prepareActionInvocation(action);
+ actionProxy.setMethod("execute");
+ actionProxy.setMethodSpecified(true);
+
+ prepareRequest("get");
+
+ String resultName = interceptor.intercept(invocation);
+
+ assertEquals("bad-request", resultName);
+ }
+
+ /**
+ * Counterpart to the above: POST against a wildcard-resolved unannotated
method must succeed
+ * when the class allows POST via {@code @AllowedHttpMethod(POST)}.
+ */
+ public void
testWildcardResolvedUnannotatedMethodAllowsPostWithClassLevelAnnotation()
throws Exception {
+ HttpMethodsTestAction action = new HttpMethodsTestAction();
+ prepareActionInvocation(action);
+ actionProxy.setMethod("execute");
+ actionProxy.setMethodSpecified(true);
+ invocation.setResultCode("success");
+
+ prepareRequest("post");
+
+ String resultName = interceptor.intercept(invocation);
+
+ assertEquals("success", resultName);
+ }
+
+ /**
+ * Exercises the full wildcard resolution path through a real {@link
com.opensymphony.xwork2.DefaultActionProxy}.
+ * <p>
+ * Config (from xwork-test-allowed-methods.xml):
+ * {@code <action name="Wild-*" class="HttpMethodsTestAction"
method="{1}">}.
+ * URL {@code Wild-execute} resolves to {@code ActionSupport.execute()} —
no method-level
+ * HTTP annotation. {@code HttpMethodsTestAction} carries class-level
+ * {@code @AllowedHttpMethod(POST)}, so GET must be rejected end-to-end.
+ */
+ public void testWildcardResolvedExecuteRejectsGetThroughRealProxy() throws
Exception {
+ loadConfigurationProviders(new StrutsXmlConfigurationProvider(
+
"com/opensymphony/xwork2/config/providers/xwork-test-allowed-methods.xml"));
+
+ MockHttpServletRequest request = new MockHttpServletRequest("GET",
"/Wild-execute");
+ Map<String, Object> extraContext = ActionContext.of()
+ .withServletRequest(request)
+ .getContextMap();
+
+ ActionProxy proxy = actionProxyFactory.createActionProxy("",
"Wild-execute", null, extraContext);
+
+ assertEquals("execute", proxy.getMethod());
+ assertTrue("Wildcard-resolved method must report
isMethodSpecified()=true", proxy.isMethodSpecified());
+
+ HttpMethodInterceptor realInterceptor = new HttpMethodInterceptor();
+ String result = realInterceptor.intercept(proxy.getInvocation());
+
+ assertEquals("bad-request", result);
+ }
+
private void prepareActionInvocation(Object action) {
interceptor = new HttpMethodInterceptor();
invocation = new MockActionInvocation();