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 5e49ebec2 WW-5838 fix(core): exclude java.beans, java.lang.reflect and
java.lang.invoke from OGNL member access
5e49ebec2 is described below
commit 5e49ebec2121250c62b456fc23cb1696ee402adc
Author: Lukasz Lenart <[email protected]>
AuthorDate: Thu Nov 5 15:15:02 2026 +0100
WW-5838 fix(core): exclude java.beans, java.lang.reflect and
java.lang.invoke from OGNL member access
SecurityMemberAccess compares the declaring class and the target class of
each
member an expression resolves against the exclusion lists.
java.beans.Expression
and java.beans.Statement perform the call they describe inside the JDK, so
the
inner call is never presented as a member and neither the excluded-class
nor the
static-method check sees it. On this line the exclusion lists are the only
layer
unless the application enables struts.allowlist.enable, so the package
exclusion
closes that path. java.lang.reflect and java.lang.invoke are reachable from
an
expression only through a static call or a Class object, both already
rejected,
and are excluded in the same pass. An application that needs one of these
classes in an expression can re-allow it with
struts.excludedPackageExemptClasses.
StrutsExcludedClassesTest pins the shipped lists with the allowlist
disabled:
member access on Expression, Statement, Method and MethodHandle is denied,
the
dev-mode list agrees, and an Expression constructed inside an OGNL
expression is
rejected at its constructor.
Co-Authored-By: Claude Mythos 5.1 <[email protected]>
---
.../src/main/resources/struts-excluded-classes.xml | 6 +
.../xwork2/ognl/StrutsExcludedClassesTest.java | 144 +++++++++++++++++++++
2 files changed, 150 insertions(+)
diff --git a/core/src/main/resources/struts-excluded-classes.xml
b/core/src/main/resources/struts-excluded-classes.xml
index a30ac86da..df0facec6 100644
--- a/core/src/main/resources/struts-excluded-classes.xml
+++ b/core/src/main/resources/struts-excluded-classes.xml
@@ -89,7 +89,10 @@
freemarker.ext.jsp,
freemarker.ext.rhino,
freemarker.template,
+ java.beans,
java.io,
+ java.lang.invoke,
+ java.lang.reflect,
java.net,
java.nio,
javassist,
@@ -127,7 +130,10 @@
freemarker.ext.jsp,
freemarker.ext.rhino,
freemarker.template,
+ java.beans,
java.io,
+ java.lang.invoke,
+ java.lang.reflect,
java.net,
java.nio,
javassist,
diff --git
a/core/src/test/java/com/opensymphony/xwork2/ognl/StrutsExcludedClassesTest.java
b/core/src/test/java/com/opensymphony/xwork2/ognl/StrutsExcludedClassesTest.java
new file mode 100644
index 000000000..fca9effa8
--- /dev/null
+++
b/core/src/test/java/com/opensymphony/xwork2/ognl/StrutsExcludedClassesTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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.ognl;
+
+import com.opensymphony.xwork2.ActionContext;
+import com.opensymphony.xwork2.XWorkTestCase;
+import
com.opensymphony.xwork2.config.providers.StrutsDefaultConfigurationProvider;
+import com.opensymphony.xwork2.inject.ContainerBuilder;
+import com.opensymphony.xwork2.test.StubConfigurationProvider;
+import com.opensymphony.xwork2.util.ValueStack;
+import com.opensymphony.xwork2.util.ValueStackFactory;
+import com.opensymphony.xwork2.util.location.LocatableProperties;
+import ognl.Ognl;
+import org.apache.struts2.StrutsConstants;
+import org.apache.struts2.config.DefaultPropertiesProvider;
+import org.apache.struts2.config.StrutsXmlConfigurationProvider;
+
+import java.beans.Expression;
+import java.beans.Statement;
+import java.beans.beancontext.BeanContextSupport;
+import java.lang.invoke.MethodHandle;
+import java.lang.reflect.Member;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Pins the exclusion lists shipped in {@code struts-excluded-classes.xml}.
The allowlist is switched off so
+ * that the exclusion lists are the only layer under test, which is the
default on this line.
+ */
+public class StrutsExcludedClassesTest extends XWorkTestCase {
+
+ private SecurityMemberAccess sma;
+ private Map<String, Object> context;
+
+ private static Map<String, String> overrides(String... keyValues) {
+ Map<String, String> overrides = new HashMap<>();
+ for (int i = 0; i < keyValues.length; i += 2) {
+ overrides.put(keyValues[i], keyValues[i + 1]);
+ }
+ return overrides;
+ }
+
+ private void loadShippedExclusions(Map<String, String> overrides) {
+ loadConfigurationProviders(
+ new StrutsDefaultConfigurationProvider(),
+ new DefaultPropertiesProvider(),
+ new
StrutsXmlConfigurationProvider("struts-excluded-classes.xml"),
+ new StubConfigurationProvider() {
+ @Override
+ public void register(ContainerBuilder builder,
LocatableProperties props) {
+
props.setProperty(StrutsConstants.STRUTS_ALLOWLIST_ENABLE, "false");
+ overrides.forEach(props::setProperty);
+ }
+ });
+ sma = ((OgnlValueStack)
container.getInstance(ValueStackFactory.class).createValueStack()).securityMemberAccess;
+ context = Ognl.createDefaultContext(null, sma);
+ }
+
+ private boolean accessible(Class<?> declaringClass, String methodName,
Class<?>... parameterTypes) throws Exception {
+ return accessibleOn(null, declaringClass, methodName, parameterTypes);
+ }
+
+ private boolean accessibleOn(Object target, Class<?> declaringClass,
String methodName, Class<?>... parameterTypes) throws Exception {
+ Member member = declaringClass.getMethod(methodName, parameterTypes);
+ return sma.isAccessible(context, target, member, null);
+ }
+
+ public void testJavaBeansIsExcluded() throws Exception {
+ loadShippedExclusions(overrides());
+
+ assertThat(accessible(Expression.class, "getValue")).isFalse();
+ assertThat(accessible(Statement.class, "execute")).isFalse();
+ assertThat(accessible(String.class, "length")).isTrue();
+ }
+
+ public void
testJavaBeansTargetIsExcludedWhenTheMemberIsDeclaredElsewhere() throws
Exception {
+ loadShippedExclusions(overrides());
+
+ assertThat(accessibleOn(new BeanContextSupport(), Collection.class,
"size")).isFalse();
+ assertThat(accessibleOn(new ArrayList<>(), Collection.class,
"size")).isTrue();
+ }
+
+ public void testReflectionPackagesAreExcluded() throws Exception {
+ loadShippedExclusions(overrides());
+
+ assertThat(accessible(Method.class, "invoke", Object.class,
Object[].class)).isFalse();
+ assertThat(accessible(MethodHandle.class, "invokeWithArguments",
Object[].class)).isFalse();
+ }
+
+ public void testExcludedPackageNamesSettingReplacesTheShippedList() throws
Exception {
+
loadShippedExclusions(overrides(StrutsConstants.STRUTS_EXCLUDED_PACKAGE_NAMES,
"antlr.build"));
+
+ assertThat(accessible(Expression.class, "getValue")).isTrue();
+ }
+
+ /**
+ * The production list is overridden to a single harmless entry, so only
the dev-mode list can be the
+ * one denying access here.
+ */
+ public void testDevModeListExcludesTheSamePackages() throws Exception {
+ loadShippedExclusions(overrides(
+ StrutsConstants.STRUTS_DEVMODE, "true",
+ StrutsConstants.STRUTS_EXCLUDED_PACKAGE_NAMES, "antlr.build"));
+
+ assertThat(accessible(Expression.class, "getValue")).isFalse();
+ assertThat(accessible(Method.class, "invoke", Object.class,
Object[].class)).isFalse();
+ assertThat(accessible(MethodHandle.class, "invokeWithArguments",
Object[].class)).isFalse();
+ assertThat(accessible(String.class, "length")).isTrue();
+ }
+
+ public void testExpressionConstructedInOgnlIsRejected() throws Exception {
+ loadShippedExclusions(overrides());
+ ValueStack stack = ActionContext.getContext().getValueStack();
+
+ Member constructor = Expression.class.getConstructor(Object.class,
String.class, Object[].class);
+ assertThat(sma.isAccessible(context, Expression.class, constructor,
null)).isFalse();
+
+ assertThat(stack.findValue("'abc'.length()", true)).isEqualTo(3);
+ assertThatThrownBy(() -> stack.findValue("new
java.beans.Expression('abc','length',null).getValue()", true))
+ .isNotNull();
+ }
+}