This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git


The following commit(s) were added to refs/heads/main by this push:
     new 7bf136a98 WW-5838 fix(core): exclude java.beans, java.lang.reflect and 
java.lang.invoke from OGNL member access
7bf136a98 is described below

commit 7bf136a980bd6aa29e6d092248a829736bb84043
Author: Lukasz Lenart <[email protected]>
AuthorDate: Thu Nov 5 15:14:27 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. The allowlist rejects the constructor on this 
line
    by default; the package exclusion closes the same path where the allowlist 
is
    disabled. 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 +
 .../struts2/ognl/StrutsExcludedClassesTest.java    | 135 +++++++++++++++++++++
 2 files changed, 141 insertions(+)

diff --git a/core/src/main/resources/struts-excluded-classes.xml 
b/core/src/main/resources/struts-excluded-classes.xml
index fbc3ee740..d42f8f7bd 100644
--- a/core/src/main/resources/struts-excluded-classes.xml
+++ b/core/src/main/resources/struts-excluded-classes.xml
@@ -79,7 +79,10 @@
                 freemarker.ext.rhino,
                 freemarker.template,
                 jakarta,
+                java.beans,
                 java.io,
+                java.lang.invoke,
+                java.lang.reflect,
                 java.net,
                 java.nio,
                 javassist,
@@ -112,7 +115,10 @@
                 freemarker.ext.rhino,
                 freemarker.template,
                 jakarta,
+                java.beans,
                 java.io,
+                java.lang.invoke,
+                java.lang.reflect,
                 java.net,
                 java.nio,
                 javassist,
diff --git 
a/core/src/test/java/org/apache/struts2/ognl/StrutsExcludedClassesTest.java 
b/core/src/test/java/org/apache/struts2/ognl/StrutsExcludedClassesTest.java
new file mode 100644
index 000000000..3233fe492
--- /dev/null
+++ b/core/src/test/java/org/apache/struts2/ognl/StrutsExcludedClassesTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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.ognl;
+
+import ognl.Ognl;
+import ognl.OgnlContext;
+import org.apache.struts2.ActionContext;
+import org.apache.struts2.StrutsConstants;
+import org.apache.struts2.XWorkTestCase;
+import org.apache.struts2.config.DefaultPropertiesProvider;
+import org.apache.struts2.config.StrutsXmlConfigurationProvider;
+import org.apache.struts2.config.providers.StrutsDefaultConfigurationProvider;
+import org.apache.struts2.inject.ContainerBuilder;
+import org.apache.struts2.test.StubConfigurationProvider;
+import org.apache.struts2.util.ValueStack;
+import org.apache.struts2.util.location.LocatableProperties;
+
+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.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, as on the 6.x line.
+ */
+public class StrutsExcludedClassesTest extends XWorkTestCase {
+
+    private SecurityMemberAccess sma;
+    private OgnlContext context;
+
+    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 = container.getInstance(SecurityMemberAccess.class);
+        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(Map.of());
+
+        assertThat(accessible(Expression.class, "getValue")).isFalse();
+        assertThat(accessible(Statement.class, "execute")).isFalse();
+        assertThat(accessible(String.class, "length")).isTrue();
+    }
+
+    public void 
testJavaBeansTargetIsExcludedWhenTheMemberIsDeclaredElsewhere() throws 
Exception {
+        loadShippedExclusions(Map.of());
+
+        assertThat(accessibleOn(new BeanContextSupport(), Collection.class, 
"size")).isFalse();
+        assertThat(accessibleOn(new ArrayList<>(), Collection.class, 
"size")).isTrue();
+    }
+
+    public void testReflectionPackagesAreExcluded() throws Exception {
+        loadShippedExclusions(Map.of());
+
+        assertThat(accessible(Method.class, "invoke", Object.class, 
Object[].class)).isFalse();
+        assertThat(accessible(MethodHandle.class, "invokeWithArguments", 
Object[].class)).isFalse();
+    }
+
+    public void testExcludedPackageNamesSettingReplacesTheShippedList() throws 
Exception {
+        
loadShippedExclusions(Map.of(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(Map.of(
+                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(Map.of());
+        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