From 875af77cadfa13abd8c011d7698feb6dfd91ff83 Mon Sep 17 00:00:00 2001
From: Lukasz Lenart
Date: Mon, 9 Feb 2026 10:57:22 +0200
Subject: [PATCH] feat(proxy): WW-5514 add StrutsProxyService for proxy
detection and resolution
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Introduces a configurable ProxyService interface and StrutsProxyService
implementation for detecting and resolving Spring AOP/Hibernate proxies.
Key changes:
- Add ProxyService interface with isProxy, ultimateTargetClass, and
resolveTargetMember methods
- Add StrutsProxyService implementation using configurable caches
- Add ProxyCacheFactory and StrutsProxyCacheFactory for cache management
- Integrate ProxyService into ChainingInterceptor, ParametersInterceptor,
and SecurityMemberAccess
- Add integration test with Spring AOP proxied action chaining
- Add configuration constants for proxy cache type and size
The StrutsProxyService correctly handles:
- Spring CGLIB proxies (class-based)
- Spring JDK dynamic proxies (interface-based)
- Hibernate entity proxies
- Member resolution for allowlist checking
Fixes WW-5514
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude
---
.../showcase/proxy/LoggingInterceptor.java | 42 ++
.../main/resources/struts-actionchaining.xml | 31 +-
apps/showcase/src/main/resources/struts.xml | 95 ++--
.../webapp/WEB-INF/applicationContext.xml | 20 +
.../SpringProxyActionChainingTest.java | 67 +++
.../org/apache/struts2/StrutsConstants.java | 29 ++
.../config/StrutsBeanSelectionProvider.java | 4 +
.../config/impl/DefaultConfiguration.java | 8 +
.../interceptor/ChainingInterceptor.java | 17 +-
.../parameter/ParametersInterceptor.java | 12 +-
.../struts2/ognl/ProxyCacheFactory.java | 27 ++
.../struts2/ognl/SecurityMemberAccess.java | 19 +-
.../struts2/ognl/StrutsProxyCacheFactory.java | 39 ++
.../org/apache/struts2/util/ProxyService.java | 101 +++++
.../org/apache/struts2/util/ProxyUtil.java | 40 +-
.../struts2/util/StrutsProxyService.java | 198 +++++++++
.../org/apache/struts2/default.properties | 13 +
core/src/main/resources/struts-beans.xml | 4 +
.../StrutsParameterAnnotationTest.java | 5 +
.../ognl/SecurityMemberAccessTest.java | 5 +
.../ognl/StrutsProxyCacheFactoryTest.java | 85 ++++
...rutsProxyServiceSpringIntegrationTest.java | 275 ++++++++++++
.../struts2/util/StrutsProxyServiceTest.java | 412 ++++++++++++++++++
.../ExternalSecurityMemberAccessTest.java | 1 +
.../struts2/json/DefaultJSONWriter.java | 10 +-
.../apache/struts2/json/JSONResultTest.java | 8 +-
.../ognl/SecurityMemberAccessProxyTest.java | 9 +-
...02-07-WW-5514-proxy-cache-configuration.md | 372 ++++++++++++++++
.../2026-02-08-WW-5514-validation.md | 175 ++++++++
29 files changed, 2044 insertions(+), 79 deletions(-)
create mode 100644 apps/showcase/src/main/java/org/apache/struts2/showcase/proxy/LoggingInterceptor.java
create mode 100644 apps/showcase/src/test/java/it/org/apache/struts2/showcase/SpringProxyActionChainingTest.java
create mode 100644 core/src/main/java/org/apache/struts2/ognl/ProxyCacheFactory.java
create mode 100644 core/src/main/java/org/apache/struts2/ognl/StrutsProxyCacheFactory.java
create mode 100644 core/src/main/java/org/apache/struts2/util/ProxyService.java
create mode 100644 core/src/main/java/org/apache/struts2/util/StrutsProxyService.java
create mode 100644 core/src/test/java/org/apache/struts2/ognl/StrutsProxyCacheFactoryTest.java
create mode 100644 core/src/test/java/org/apache/struts2/util/StrutsProxyServiceSpringIntegrationTest.java
create mode 100644 core/src/test/java/org/apache/struts2/util/StrutsProxyServiceTest.java
create mode 100644 thoughts/shared/research/2026-02-07-WW-5514-proxy-cache-configuration.md
create mode 100644 thoughts/shared/validation/2026-02-08-WW-5514-validation.md
diff --git a/apps/showcase/src/main/java/org/apache/struts2/showcase/proxy/LoggingInterceptor.java b/apps/showcase/src/main/java/org/apache/struts2/showcase/proxy/LoggingInterceptor.java
new file mode 100644
index 0000000000..4a5dd40a7c
--- /dev/null
+++ b/apps/showcase/src/main/java/org/apache/struts2/showcase/proxy/LoggingInterceptor.java
@@ -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 org.apache.struts2.showcase.proxy;
+
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+/**
+ * Simple AOP interceptor that wraps actions in a Spring proxy.
+ * Used to test that Struts correctly handles Spring AOP proxied actions
+ * in action chaining scenarios (WW-5514).
+ */
+public class LoggingInterceptor implements MethodInterceptor {
+
+ private static final Logger LOG = LogManager.getLogger(LoggingInterceptor.class);
+
+ @Override
+ public Object invoke(MethodInvocation invocation) throws Throwable {
+ LOG.debug("Invoking method: {} on target: {}",
+ invocation.getMethod().getName(),
+ invocation.getThis().getClass().getName());
+ return invocation.proceed();
+ }
+}
diff --git a/apps/showcase/src/main/resources/struts-actionchaining.xml b/apps/showcase/src/main/resources/struts-actionchaining.xml
index 4f39940f06..ae2a7461c8 100644
--- a/apps/showcase/src/main/resources/struts-actionchaining.xml
+++ b/apps/showcase/src/main/resources/struts-actionchaining.xml
@@ -20,21 +20,26 @@
*/
-->
+ "-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
+ "https://struts.apache.org/dtds/struts-6.0.dtd">
-
-
- actionChain2
-
-
- actionChain3
-
-
- /WEB-INF/actionchaining/actionChainingResult.jsp
-
-
+
+
+ actionChain2
+
+
+ actionChain3
+
+
+ /WEB-INF/actionchaining/actionChainingResult.jsp
+
+
+
+
+ actionChain2
+
+
diff --git a/apps/showcase/src/main/resources/struts.xml b/apps/showcase/src/main/resources/struts.xml
index 5c1cf37ff8..5dbe07cee5 100644
--- a/apps/showcase/src/main/resources/struts.xml
+++ b/apps/showcase/src/main/resources/struts.xml
@@ -20,83 +20,88 @@
*/
-->
+ "-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
+ "https://struts.apache.org/dtds/struts-6.0.dtd">
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
-
+
-
-
-
+
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
-
-
-
+
+
+
+
-
+ /WEB-INF/showcase.jsp
@@ -125,7 +130,7 @@
/WEB-INF/empmanager/editSkill.jsp
-
+
@@ -146,9 +151,11 @@
- {1}
+ {1}
/WEB-INF/empmanager/editEmployee.jsp
- execute
+
+ execute
+ /WEB-INF/empmanager/editEmployee.jsp
@@ -168,5 +175,5 @@
-
+
diff --git a/apps/showcase/src/main/webapp/WEB-INF/applicationContext.xml b/apps/showcase/src/main/webapp/WEB-INF/applicationContext.xml
index ef700ef48f..788890326b 100644
--- a/apps/showcase/src/main/webapp/WEB-INF/applicationContext.xml
+++ b/apps/showcase/src/main/webapp/WEB-INF/applicationContext.xml
@@ -115,5 +115,25 @@
+
+
+
+
+
+
+
+
+
+ proxiedActionChain1
+
+
+
+
+ loggingInterceptor
+
+
+
+
+
diff --git a/apps/showcase/src/test/java/it/org/apache/struts2/showcase/SpringProxyActionChainingTest.java b/apps/showcase/src/test/java/it/org/apache/struts2/showcase/SpringProxyActionChainingTest.java
new file mode 100644
index 0000000000..8b3a9794c9
--- /dev/null
+++ b/apps/showcase/src/test/java/it/org/apache/struts2/showcase/SpringProxyActionChainingTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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 it.org.apache.struts2.showcase;
+
+import org.htmlunit.WebClient;
+import org.htmlunit.html.HtmlPage;
+import org.junit.Test;
+
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Integration test verifying that Spring AOP proxied actions work correctly
+ * with action chaining. This tests the WW-5514 StrutsProxyService integration.
+ *
+ *
The test uses a Spring AOP proxied version of ActionChain1 (proxiedActionChain1)
+ * which is wrapped by {@link org.apache.struts2.showcase.proxy.LoggingInterceptor}.
+ * The ChainingInterceptor must correctly resolve the target class through
+ * StrutsProxyService to copy properties to the next action in the chain.
+ */
+public class SpringProxyActionChainingTest {
+
+ /**
+ * Tests that action chaining works correctly when the first action is a Spring AOP proxy.
+ *
+ *
This verifies that:
+ *
+ *
StrutsProxyService correctly identifies the Spring CGLIB proxy
+ *
ChainingInterceptor resolves the target class for property copying
+ *
Properties from the proxied ActionChain1 are correctly copied to ActionChain2
+ *
+ *
+ */
+ @Test
+ public void testProxiedActionChaining() throws Exception {
+ try (final WebClient webClient = new WebClient()) {
+ final HtmlPage page = webClient.getPage(
+ ParameterUtils.getBaseUrl() + "/actionchaining/proxiedActionChain1!input"
+ );
+
+ final String pageAsText = page.asNormalizedText();
+
+ // Verify properties were chained correctly despite proxy
+ assertTrue("ActionChain1 property should be present",
+ pageAsText.contains("Action Chain 1 Property 1: Property Set In Action Chain 1"));
+ assertTrue("ActionChain2 property should be present",
+ pageAsText.contains("Action Chain 2 Property 1: Property Set in Action Chain 2"));
+ assertTrue("ActionChain3 property should be present",
+ pageAsText.contains("Action Chain 3 Property 1: Property set in Action Chain 3"));
+ }
+ }
+}
diff --git a/core/src/main/java/org/apache/struts2/StrutsConstants.java b/core/src/main/java/org/apache/struts2/StrutsConstants.java
index a66478a97c..418c55e440 100644
--- a/core/src/main/java/org/apache/struts2/StrutsConstants.java
+++ b/core/src/main/java/org/apache/struts2/StrutsConstants.java
@@ -516,6 +516,35 @@ public final class StrutsConstants {
*/
public static final String STRUTS_OGNL_EXPRESSION_CACHE_MAXSIZE = "struts.ognl.expressionCacheMaxSize";
+ /**
+ * Specifies the type of cache to use for proxy detection. Valid values defined in
+ * {@link org.apache.struts2.ognl.OgnlCacheFactory.CacheType}.
+ *
+ * @since 7.2.0
+ */
+ public static final String STRUTS_PROXY_CACHE_TYPE = "struts.proxy.cacheType";
+
+ /**
+ * Specifies the maximum cache size for proxy detection caches.
+ *
+ * @since 7.2.0
+ */
+ public static final String STRUTS_PROXY_CACHE_MAXSIZE = "struts.proxy.cacheMaxSize";
+
+ /**
+ * The {@link org.apache.struts2.ognl.ProxyCacheFactory} implementation class.
+ *
+ * @since 7.2.0
+ */
+ public static final String STRUTS_PROXY_CACHE_FACTORY = "struts.proxy.cacheFactory";
+
+ /**
+ * The {@link org.apache.struts2.util.ProxyService} implementation class.
+ *
+ * @since 7.2.0
+ */
+ public static final String STRUTS_PROXYSERVICE = "struts.proxyService";
+
/**
* Enables evaluation of OGNL expressions
*
diff --git a/core/src/main/java/org/apache/struts2/config/StrutsBeanSelectionProvider.java b/core/src/main/java/org/apache/struts2/config/StrutsBeanSelectionProvider.java
index eda65e527f..c584a4f587 100644
--- a/core/src/main/java/org/apache/struts2/config/StrutsBeanSelectionProvider.java
+++ b/core/src/main/java/org/apache/struts2/config/StrutsBeanSelectionProvider.java
@@ -61,6 +61,7 @@
import org.apache.struts2.ognl.BeanInfoCacheFactory;
import org.apache.struts2.ognl.ExpressionCacheFactory;
import org.apache.struts2.ognl.OgnlGuard;
+import org.apache.struts2.ognl.ProxyCacheFactory;
import org.apache.struts2.ognl.SecurityMemberAccess;
import org.apache.struts2.ognl.accessor.RootAccessor;
import org.apache.struts2.security.AcceptedPatternsChecker;
@@ -72,6 +73,7 @@
import org.apache.struts2.url.UrlEncoder;
import org.apache.struts2.util.ContentTypeMatcher;
import org.apache.struts2.util.PatternMatcher;
+import org.apache.struts2.util.ProxyService;
import org.apache.struts2.util.TextParser;
import org.apache.struts2.util.ValueStackFactory;
import org.apache.struts2.util.location.LocatableProperties;
@@ -442,6 +444,8 @@ public void register(ContainerBuilder builder, LocatableProperties props) {
alias(ExpressionCacheFactory.class, StrutsConstants.STRUTS_OGNL_EXPRESSION_CACHE_FACTORY, builder, props, Scope.SINGLETON);
alias(BeanInfoCacheFactory.class, StrutsConstants.STRUTS_OGNL_BEANINFO_CACHE_FACTORY, builder, props, Scope.SINGLETON);
+ alias(ProxyCacheFactory.class, StrutsConstants.STRUTS_PROXY_CACHE_FACTORY, builder, props, Scope.SINGLETON);
+ alias(ProxyService.class, StrutsConstants.STRUTS_PROXYSERVICE, builder, props, Scope.SINGLETON);
alias(SecurityMemberAccess.class, StrutsConstants.STRUTS_MEMBER_ACCESS, builder, props, Scope.PROTOTYPE);
alias(OgnlGuard.class, StrutsConstants.STRUTS_OGNL_GUARD, builder, props, Scope.SINGLETON);
diff --git a/core/src/main/java/org/apache/struts2/config/impl/DefaultConfiguration.java b/core/src/main/java/org/apache/struts2/config/impl/DefaultConfiguration.java
index 48791c9191..9ec9f9c20e 100644
--- a/core/src/main/java/org/apache/struts2/config/impl/DefaultConfiguration.java
+++ b/core/src/main/java/org/apache/struts2/config/impl/DefaultConfiguration.java
@@ -85,13 +85,17 @@
import org.apache.struts2.ognl.OgnlCacheFactory;
import org.apache.struts2.ognl.OgnlReflectionProvider;
import org.apache.struts2.ognl.OgnlUtil;
+import org.apache.struts2.ognl.ProxyCacheFactory;
+import org.apache.struts2.ognl.StrutsProxyCacheFactory;
import org.apache.struts2.ognl.OgnlValueStackFactory;
import org.apache.struts2.ognl.SecurityMemberAccess;
import org.apache.struts2.ognl.accessor.CompoundRootAccessor;
import org.apache.struts2.ognl.accessor.RootAccessor;
import org.apache.struts2.ognl.accessor.XWorkMethodAccessor;
+import org.apache.struts2.util.StrutsProxyService;
import org.apache.struts2.util.OgnlTextParser;
import org.apache.struts2.util.PatternMatcher;
+import org.apache.struts2.util.ProxyService;
import org.apache.struts2.text.StrutsLocalizedTextProvider;
import org.apache.struts2.util.TextParser;
import org.apache.struts2.util.ValueStack;
@@ -144,6 +148,8 @@ public class DefaultConfiguration implements Configuration {
constants.put(StrutsConstants.STRUTS_OGNL_EXPRESSION_CACHE_MAXSIZE, 10000);
constants.put(StrutsConstants.STRUTS_OGNL_BEANINFO_CACHE_TYPE, OgnlCacheFactory.CacheType.BASIC);
constants.put(StrutsConstants.STRUTS_OGNL_BEANINFO_CACHE_MAXSIZE, 10000);
+ constants.put(StrutsConstants.STRUTS_PROXY_CACHE_TYPE, OgnlCacheFactory.CacheType.BASIC);
+ constants.put(StrutsConstants.STRUTS_PROXY_CACHE_MAXSIZE, 10000);
constants.put(StrutsConstants.STRUTS_ENABLE_DYNAMIC_METHOD_INVOCATION, Boolean.FALSE);
BOOTSTRAP_CONSTANTS = Collections.unmodifiableMap(constants);
}
@@ -395,6 +401,8 @@ public static ContainerBuilder bootstrapFactories(ContainerBuilder builder) {
.factory(ExpressionCacheFactory.class, DefaultOgnlExpressionCacheFactory.class, Scope.SINGLETON)
.factory(BeanInfoCacheFactory.class, DefaultOgnlBeanInfoCacheFactory.class, Scope.SINGLETON)
+ .factory(ProxyCacheFactory.class, StrutsProxyCacheFactory.class, Scope.SINGLETON)
+ .factory(ProxyService.class, StrutsProxyService.class, Scope.SINGLETON)
.factory(OgnlUtil.class, Scope.SINGLETON)
.factory(SecurityMemberAccess.class, Scope.PROTOTYPE)
.factory(OgnlGuard.class, StrutsOgnlGuard.class, Scope.SINGLETON)
diff --git a/core/src/main/java/org/apache/struts2/interceptor/ChainingInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/ChainingInterceptor.java
index 2837d4c105..9c18d88696 100644
--- a/core/src/main/java/org/apache/struts2/interceptor/ChainingInterceptor.java
+++ b/core/src/main/java/org/apache/struts2/interceptor/ChainingInterceptor.java
@@ -27,7 +27,7 @@
import org.apache.struts2.result.ActionChainResult;
import org.apache.struts2.result.Result;
import org.apache.struts2.util.CompoundRoot;
-import org.apache.struts2.util.ProxyUtil;
+import org.apache.struts2.util.ProxyService;
import org.apache.struts2.util.TextParseUtil;
import org.apache.struts2.util.ValueStack;
import org.apache.struts2.util.reflection.ReflectionProvider;
@@ -96,7 +96,7 @@
*
*
* Example code:
- *
+ *
*
*
- *
* @author mrdon
* @author tm_jee ( tm_jee(at)yahoo.co.uk )
* @see ActionChainResult
@@ -135,12 +134,18 @@ public class ChainingInterceptor extends AbstractInterceptor {
protected Collection includes;
protected ReflectionProvider reflectionProvider;
+ private ProxyService proxyService;
@Inject
public void setReflectionProvider(ReflectionProvider prov) {
this.reflectionProvider = prov;
}
+ @Inject
+ public void setProxyService(ProxyService proxyService) {
+ this.proxyService = proxyService;
+ }
+
@Inject(value = StrutsConstants.STRUTS_CHAINING_COPY_ERRORS, required = false)
public void setCopyErrors(String copyErrors) {
this.copyErrors = "true".equalsIgnoreCase(copyErrors);
@@ -175,8 +180,8 @@ private void copyStack(ActionInvocation invocation, CompoundRoot root) {
}
Object action = invocation.getAction();
Class> editable = null;
- if (ProxyUtil.isProxy(action)) {
- editable = ProxyUtil.ultimateTargetClass(action);
+ if (proxyService.isProxy(action)) {
+ editable = proxyService.ultimateTargetClass(action);
}
reflectionProvider.copy(object, action, ctxMap, prepareExcludes(), includes, editable);
}
@@ -184,7 +189,7 @@ private void copyStack(ActionInvocation invocation, CompoundRoot root) {
private Collection prepareExcludes() {
Collection localExcludes = excludes;
- if (!copyErrors || !copyMessages ||!copyFieldErrors) {
+ if (!copyErrors || !copyMessages || !copyFieldErrors) {
if (localExcludes == null) {
localExcludes = new HashSet<>();
if (!copyErrors) {
diff --git a/core/src/main/java/org/apache/struts2/interceptor/parameter/ParametersInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/parameter/ParametersInterceptor.java
index 32cffc291f..293f4968a9 100644
--- a/core/src/main/java/org/apache/struts2/interceptor/parameter/ParametersInterceptor.java
+++ b/core/src/main/java/org/apache/struts2/interceptor/parameter/ParametersInterceptor.java
@@ -39,7 +39,7 @@
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.ProxyService;
import org.apache.struts2.util.TextParseUtil;
import org.apache.struts2.util.ValueStack;
import org.apache.struts2.util.ValueStackFactory;
@@ -95,6 +95,7 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
private ValueStackFactory valueStackFactory;
private OgnlUtil ognlUtil;
protected ThreadAllowlist threadAllowlist;
+ private ProxyService proxyService;
private ExcludedPatternsChecker excludedPatterns;
private AcceptedPatternsChecker acceptedPatterns;
private Set excludedValuePatterns = null;
@@ -115,6 +116,11 @@ public void setThreadAllowlist(ThreadAllowlist threadAllowlist) {
this.threadAllowlist = threadAllowlist;
}
+ @Inject
+ public void setProxyService(ProxyService proxyService) {
+ this.proxyService = proxyService;
+ }
+
@Inject(StrutsConstants.STRUTS_DEVMODE)
public void setDevMode(String mode) {
this.devMode = BooleanUtils.toBoolean(mode);
@@ -516,8 +522,8 @@ protected StrutsParameter getParameterAnnotation(AnnotatedElement element) {
}
protected Class> ultimateClass(Object action) {
- if (ProxyUtil.isProxy(action)) {
- return ProxyUtil.ultimateTargetClass(action);
+ if (proxyService.isProxy(action)) {
+ return proxyService.ultimateTargetClass(action);
}
return action.getClass();
}
diff --git a/core/src/main/java/org/apache/struts2/ognl/ProxyCacheFactory.java b/core/src/main/java/org/apache/struts2/ognl/ProxyCacheFactory.java
new file mode 100644
index 0000000000..1243b6f24a
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/ognl/ProxyCacheFactory.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2022 Apache Software Foundation.
+ *
+ * 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 org.apache.struts2.ognl;
+
+/**
+ * A proxy interface to be used with Struts DI mechanism for proxy detection caching.
+ *
+ * @param <Key> The type for the cache key entries
+ * @param <Value> The type for the cache value entries
+ * @since 7.2.0
+ */
+public interface ProxyCacheFactory extends OgnlCacheFactory {
+
+}
diff --git a/core/src/main/java/org/apache/struts2/ognl/SecurityMemberAccess.java b/core/src/main/java/org/apache/struts2/ognl/SecurityMemberAccess.java
index 035a685bf8..d25bbe3774 100644
--- a/core/src/main/java/org/apache/struts2/ognl/SecurityMemberAccess.java
+++ b/core/src/main/java/org/apache/struts2/ognl/SecurityMemberAccess.java
@@ -25,7 +25,7 @@
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.inject.Inject;
-import org.apache.struts2.util.ProxyUtil;
+import org.apache.struts2.util.ProxyService;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
@@ -76,6 +76,8 @@ public class SecurityMemberAccess implements MemberAccess {
private final ProviderAllowlist providerAllowlist;
private final ThreadAllowlist threadAllowlist;
+ private ProxyService proxyService;
+
private boolean allowStaticFieldAccess = true;
private Set excludeProperties = emptySet();
@@ -107,6 +109,11 @@ public SecurityMemberAccess(@Inject ProviderAllowlist providerAllowlist, @Inject
this.threadAllowlist = threadAllowlist;
}
+ @Inject
+ public void setProxyService(ProxyService proxyService) {
+ this.proxyService = proxyService;
+ }
+
@Override
public Object setup(OgnlContext context, Object target, Member member, String propertyName) {
Object result = null;
@@ -214,15 +221,15 @@ protected boolean checkAllowlist(Object target, Member member) {
Class> targetClass = target != null ? target.getClass() : null;
- if (!disallowProxyObjectAccess && ProxyUtil.isProxy(target)) {
+ if (!disallowProxyObjectAccess && proxyService.isProxy(target)) {
// If `disallowProxyObjectAccess` is not set, allow resolving Hibernate entities and Spring proxies to their
// underlying classes/members. This allows the allowlist capability to continue working and still offer
// protection in applications where the developer has accepted the risk of allowing OGNL access to Hibernate
// entities and Spring proxies. This is preferred to having to disable the allowlist capability entirely.
- Class> newTargetClass = ProxyUtil.ultimateTargetClass(target);
+ Class> newTargetClass = proxyService.ultimateTargetClass(target);
if (newTargetClass != targetClass) {
targetClass = newTargetClass;
- member = ProxyUtil.resolveTargetMember(member, newTargetClass);
+ member = proxyService.resolveTargetMember(member, newTargetClass);
}
}
@@ -312,14 +319,14 @@ protected boolean checkDefaultPackageAccess(Object target, Member member) {
* @return {@code true} if proxy object access is allowed
*/
protected boolean checkProxyObjectAccess(Object target) {
- return !(disallowProxyObjectAccess && ProxyUtil.isProxy(target));
+ return !(disallowProxyObjectAccess && proxyService.isProxy(target));
}
/**
* @return {@code true} if proxy member access is allowed
*/
protected boolean checkProxyMemberAccess(Object target, Member member) {
- return !(disallowProxyMemberAccess && ProxyUtil.isProxyMember(member, target));
+ return !(disallowProxyMemberAccess && proxyService.isProxyMember(member, target));
}
/**
diff --git a/core/src/main/java/org/apache/struts2/ognl/StrutsProxyCacheFactory.java b/core/src/main/java/org/apache/struts2/ognl/StrutsProxyCacheFactory.java
new file mode 100644
index 0000000000..ac80163af9
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/ognl/StrutsProxyCacheFactory.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2022 Apache Software Foundation.
+ *
+ * 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 org.apache.struts2.ognl;
+
+import org.apache.commons.lang3.EnumUtils;
+import org.apache.struts2.StrutsConstants;
+import org.apache.struts2.inject.Inject;
+
+/**
+ * Struts proxy cache factory implementation.
+ * Used for creating caches for proxy detection operations.
+ *
+ * @param <Key> The type for the cache key entries
+ * @param <Value> The type for the cache value entries
+ * @since 7.2.0
+ */
+public class StrutsProxyCacheFactory extends DefaultOgnlCacheFactory
+ implements ProxyCacheFactory {
+
+ @Inject
+ public StrutsProxyCacheFactory(
+ @Inject(value = StrutsConstants.STRUTS_PROXY_CACHE_MAXSIZE) String cacheMaxSize,
+ @Inject(value = StrutsConstants.STRUTS_PROXY_CACHE_TYPE) String defaultCacheType) {
+ super(Integer.parseInt(cacheMaxSize), EnumUtils.getEnumIgnoreCase(CacheType.class, defaultCacheType));
+ }
+}
diff --git a/core/src/main/java/org/apache/struts2/util/ProxyService.java b/core/src/main/java/org/apache/struts2/util/ProxyService.java
new file mode 100644
index 0000000000..fe6aca7ae1
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/util/ProxyService.java
@@ -0,0 +1,101 @@
+/*
+ * 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.util;
+
+import java.lang.reflect.Member;
+
+/**
+ * Service interface for proxy detection and resolution operations.
+ * Replaces static {@link ProxyUtil} methods with an injectable service.
+ *
+ * @since 7.2.0
+ */
+public interface ProxyService {
+
+ /**
+ * Determine the ultimate target class of the given instance, traversing
+ * not only a top-level proxy but any number of nested proxies as well —
+ * as long as possible without side effects.
+ *
+ * @param candidate the instance to check (might be a proxy)
+ * @return the ultimate target class (or the plain class of the given
+ * object as fallback; never {@code null})
+ */
+ Class> ultimateTargetClass(Object candidate);
+
+ /**
+ * Check whether the given object is a proxy.
+ *
+ * @param object the object to check
+ * @return true if the object is a Spring AOP or Hibernate proxy
+ */
+ boolean isProxy(Object object);
+
+ /**
+ * Check whether the given member is a proxy member of a proxy object or is a static proxy member.
+ *
+ * @param member the member to check
+ * @param object the object to check
+ * @return true if the member is a proxy member
+ */
+ boolean isProxyMember(Member member, Object object);
+
+ /**
+ * Check whether the given object is a Hibernate proxy.
+ *
+ * @param object the object to check
+ * @return true if the object is a Hibernate proxy
+ */
+ boolean isHibernateProxy(Object object);
+
+ /**
+ * Check whether the given member is a member of a Hibernate proxy.
+ *
+ * @param member the member to check
+ * @return true if the member is a Hibernate proxy member
+ */
+ boolean isHibernateProxyMember(Member member);
+
+ /**
+ * Get the target instance of the given object if it is a Hibernate proxy object,
+ * otherwise return the given object.
+ *
+ * @param object the object to check
+ * @return the target instance or the original object
+ */
+ Object getHibernateProxyTarget(Object object);
+
+ /**
+ * Resolve matching member on target class.
+ *
+ * @param proxyMember the proxy member
+ * @param targetClass the target class
+ * @return matching member on target object if one exists, otherwise the same member
+ */
+ Member resolveTargetMember(Member proxyMember, Class> targetClass);
+
+ /**
+ * @param proxyMember the proxy member
+ * @param target the target object
+ * @return matching member on target object if one exists, otherwise the same member
+ * @deprecated since 7.1, use {@link #resolveTargetMember(Member, Class)} instead.
+ */
+ @Deprecated
+ Member resolveTargetMember(Member proxyMember, Object target);
+}
diff --git a/core/src/main/java/org/apache/struts2/util/ProxyUtil.java b/core/src/main/java/org/apache/struts2/util/ProxyUtil.java
index a574af1c22..1260a7dee6 100644
--- a/core/src/main/java/org/apache/struts2/util/ProxyUtil.java
+++ b/core/src/main/java/org/apache/struts2/util/ProxyUtil.java
@@ -43,10 +43,12 @@
/**
* ProxyUtil
*
- * Various utility methods dealing with proxies
+ * Various utility methods dealing with proxies.
*
*
+ * @deprecated since 7.2, inject {@link ProxyService} instead. This class will be removed in a future version.
*/
+@Deprecated(since = "7.2")
public class ProxyUtil {
private static final int CACHE_MAX_SIZE = 10000;
private static final int CACHE_INITIAL_CAPACITY = 256;
@@ -61,10 +63,13 @@ public class ProxyUtil {
* Determine the ultimate target class of the given instance, traversing
* not only a top-level proxy but any number of nested proxies as well —
* as long as possible without side effects.
+ *
* @param candidate the instance to check (might be a proxy)
* @return the ultimate target class (or the plain class of the given
* object as fallback; never {@code null})
+ * @deprecated since 7.2, inject {@link ProxyService} instead
*/
+ @Deprecated(since = "7.2")
public static Class> ultimateTargetClass(Object candidate) {
return targetClassCache.computeIfAbsent(candidate, k -> {
Class> result = null;
@@ -82,8 +87,12 @@ public static Class> ultimateTargetClass(Object candidate) {
/**
* Check whether the given object is a proxy.
+ *
* @param object the object to check
+ * @return true if the object is a Spring AOP or Hibernate proxy
+ * @deprecated since 7.2, inject {@link ProxyService} instead
*/
+ @Deprecated(since = "7.2")
public static boolean isProxy(Object object) {
if (object == null) return false;
return isProxyCache.computeIfAbsent(object.getClass(),
@@ -92,9 +101,13 @@ public static boolean isProxy(Object object) {
/**
* Check whether the given member is a proxy member of a proxy object or is a static proxy member.
+ *
* @param member the member to check
* @param object the object to check
+ * @return true if the member is a proxy member
+ * @deprecated since 7.2, inject {@link ProxyService} instead
*/
+ @Deprecated(since = "7.2")
public static boolean isProxyMember(Member member, Object object) {
if (!isStatic(member.getModifiers()) && !isProxy(object)) {
return false;
@@ -107,7 +120,10 @@ public static boolean isProxyMember(Member member, Object object) {
* Check whether the given object is a Hibernate proxy.
*
* @param object the object to check
+ * @return true if the object is a Hibernate proxy
+ * @deprecated since 7.2, inject {@link ProxyService} instead
*/
+ @Deprecated(since = "7.2")
public static boolean isHibernateProxy(Object object) {
try {
return object != null && HibernateProxy.class.isAssignableFrom(object.getClass());
@@ -120,7 +136,10 @@ public static boolean isHibernateProxy(Object object) {
* Check whether the given member is a member of a Hibernate proxy.
*
* @param member the member to check
+ * @return true if the member is a Hibernate proxy member
+ * @deprecated since 7.2, inject {@link ProxyService} instead
*/
+ @Deprecated(since = "7.2")
public static boolean isHibernateProxyMember(Member member) {
try {
return hasMember(HibernateProxy.class, member);
@@ -133,6 +152,7 @@ public static boolean isHibernateProxyMember(Member member) {
* Determine the ultimate target class of the given spring bean instance, traversing
* not only a top-level spring proxy but any number of nested spring proxies as well —
* as long as possible without side effects, that is, just for singleton targets.
+ *
* @param candidate the instance to check (might be a spring AOP proxy)
* @return the ultimate target class (or the plain class of the given
* object as fallback; never {@code null})
@@ -147,6 +167,7 @@ private static Class> springUltimateTargetClass(Object candidate) {
/**
* Check whether the given object is a Spring proxy.
+ *
* @param object the object to check
*/
private static boolean isSpringAopProxy(Object object) {
@@ -159,6 +180,7 @@ private static boolean isSpringAopProxy(Object object) {
/**
* Check whether the given member is a member of a spring proxy.
+ *
* @param member the member to check
*/
private static boolean isSpringProxyMember(Member member) {
@@ -176,7 +198,8 @@ private static boolean isSpringProxyMember(Member member) {
/**
* Check whether the given class has a given member.
- * @param clazz the class to check
+ *
+ * @param clazz the class to check
* @param member the member to check
*/
private static boolean hasMember(Class> clazz, Member member) {
@@ -193,8 +216,13 @@ private static boolean hasMember(Class> clazz, Member member) {
}
/**
+ * Get the target instance of the given object if it is a Hibernate proxy object.
+ *
+ * @param object the object to check
* @return the target instance of the given object if it is a Hibernate proxy object, otherwise the given object
+ * @deprecated since 7.2, inject {@link ProxyService} instead
*/
+ @Deprecated(since = "7.2")
public static Object getHibernateProxyTarget(Object object) {
try {
return Hibernate.unproxy(object);
@@ -204,9 +232,15 @@ public static Object getHibernateProxyTarget(Object object) {
}
/**
+ * Resolve matching member on target object.
+ *
+ * @param proxyMember the proxy member
+ * @param target the target object
+ * @return matching member on target object if one exists, otherwise the same member
* @deprecated since 7.1, use {@link #resolveTargetMember(Member, Class)} instead.
+ * Since 7.2, inject {@link ProxyService} instead.
*/
- @Deprecated
+ @Deprecated(since = "7.1")
public static Member resolveTargetMember(Member proxyMember, Object target) {
return resolveTargetMember(proxyMember, target.getClass());
}
diff --git a/core/src/main/java/org/apache/struts2/util/StrutsProxyService.java b/core/src/main/java/org/apache/struts2/util/StrutsProxyService.java
new file mode 100644
index 0000000000..8d3e54798b
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/util/StrutsProxyService.java
@@ -0,0 +1,198 @@
+/*
+ * 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.util;
+
+import org.apache.commons.lang3.reflect.ConstructorUtils;
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.commons.lang3.reflect.MethodUtils;
+import org.apache.struts2.inject.Inject;
+import org.apache.struts2.ognl.OgnlCache;
+import org.apache.struts2.ognl.ProxyCacheFactory;
+import org.hibernate.Hibernate;
+import org.hibernate.proxy.HibernateProxy;
+import org.springframework.aop.TargetClassAware;
+import org.springframework.aop.framework.Advised;
+import org.springframework.aop.framework.AopProxyUtils;
+import org.springframework.aop.support.AopUtils;
+import org.springframework.aop.SpringProxy;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Member;
+import java.lang.reflect.Method;
+
+import static java.lang.reflect.Modifier.isPublic;
+import static java.lang.reflect.Modifier.isStatic;
+
+/**
+ * Default implementation of {@link ProxyService}.
+ * Provides proxy detection and resolution for Spring AOP and Hibernate proxies.
+ *
+ * @since 7.2.0
+ */
+public class StrutsProxyService implements ProxyService {
+
+ private final OgnlCache, Boolean> isProxyCache;
+ private final OgnlCache isProxyMemberCache;
+ private final OgnlCache