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();
+    }
+}

Reply via email to