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 74d7ec0d7 WW-5677 perf(ognl): drop the residual getPackage() lookups 
on the access path (#1859)
74d7ec0d7 is described below

commit 74d7ec0d7464062eb19e5b7018e9cc210191f24a
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sun Aug 23 19:06:38 2026 +0200

    WW-5677 perf(ognl): drop the residual getPackage() lookups on the access 
path (#1859)
    
    WW-5674 replaced Class.getPackage() with the cached Class.getPackageName()
    in toPackageName, but deliberately left two neighbouring call sites alone
    as out of scope. Both sit on the same per-access path within twenty lines
    of it.
    
    checkDefaultPackageAccess still tested
    `getPackage() == null || getPackage().getName().isEmpty()`, which resolves
    through the defining classloader's package map twice per class, for up to
    two classes per access. It now tests toPackageName(clazz).isEmpty(). The
    two forms agree for every class shape: getPackage() is null for arrays,
    primitives and void, and names the unnamed package with the empty string,
    all of which toPackageName reports as empty.
    
    isExcludedPackageNamePatterns evaluated toPackageName inside the lambda,
    so it re-resolved the package name once per configured pattern. It is now
    resolved once per call.
    
    Both changes are behaviour-preserving. Per the ticket, the equivalence is
    asserted rather than argued: 
defaultPackageConditionMatchesLegacyAcrossClassShapes
    runs the replaced condition, frozen verbatim as an oracle that calls
    getPackage() directly, against the new one over the existing class-shape
    matrix -- arrays, primitives, void, a default-package class, a lambda and
    a JDK proxy. Two behavioural tests cover the gate itself: a named-package
    class still passes, and an array target, the shape most likely to break
    the equivalence, stays blocked.
    
    All three tests were mutation-checked. Resolving arrays to java.lang fails
    the equivalence test and the array-target test; inverting the member-class
    condition fails the named-package test.
    
    Neither path runs by default -- checkDefaultPackageAccess only when
    struts.disallowDefaultPackageAccess is enabled, and the pattern loop only
    when struts.excludedPackageNamePatterns is configured, both commented out
    in struts-excluded-classes.xml.
    
    Co-authored-by: Claude Opus 5 <[email protected]>
---
 .../apache/struts2/ognl/SecurityMemberAccess.java  | 17 +++++++++++---
 .../SecurityMemberAccessPackageMatchingTest.java   | 24 +++++++++++++++++++
 .../struts2/ognl/SecurityMemberAccessTest.java     | 27 ++++++++++++++++++++++
 3 files changed, 65 insertions(+), 3 deletions(-)

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 4d552d594..10fdf65ce 100644
--- a/core/src/main/java/org/apache/struts2/ognl/SecurityMemberAccess.java
+++ b/core/src/main/java/org/apache/struts2/ognl/SecurityMemberAccess.java
@@ -310,6 +310,15 @@ public class SecurityMemberAccess implements MemberAccess {
     }
 
     /**
+     * Blocks access to classes in the default (unnamed) package.
+     * <p>
+     * The emptiness of {@link #toPackageName(Class)} is the same test as the
+     * {@code getPackage() == null || getPackage().getName().isEmpty()} form 
this replaced, for every
+     * class shape: {@code getPackage()} is null for arrays, primitives and 
{@code void}, and names the
+     * unnamed package with the empty string, all of which {@code 
toPackageName} reports as empty. It
+     * avoids the {@code getPackage()} lookup through the defining 
classloader's package map, which
+     * ran twice per class here. See WW-5677.
+     *
      * @return {@code true} if member access is allowed
      */
     protected boolean checkDefaultPackageAccess(Object target, Member member) {
@@ -317,7 +326,7 @@ public class SecurityMemberAccess implements MemberAccess {
             return true;
         }
         Class<?> memberClass = member.getDeclaringClass();
-        if (memberClass.getPackage() == null || 
memberClass.getPackage().getName().isEmpty()) {
+        if (toPackageName(memberClass).isEmpty()) {
             LOG.warn("Class [{}] from the default package is excluded!", 
memberClass);
             return false;
         }
@@ -325,7 +334,7 @@ public class SecurityMemberAccess implements MemberAccess {
             return true;
         }
         Class<?> targetClass = target.getClass();
-        if (targetClass.getPackage() == null || 
targetClass.getPackage().getName().isEmpty()) {
+        if (toPackageName(targetClass).isEmpty()) {
             LOG.warn("Class [{}] from the default package is excluded!", 
targetClass);
             return false;
         }
@@ -407,7 +416,9 @@ public class SecurityMemberAccess implements MemberAccess {
     }
 
     protected boolean isExcludedPackageNamePatterns(Class<?> clazz) {
-        return excludedPackageNamePatterns.stream().anyMatch(pattern -> 
pattern.matcher(toPackageName(clazz)).matches());
+        // Resolved once rather than inside the lambda, which re-resolved it 
per configured pattern.
+        String packageName = toPackageName(clazz);
+        return excludedPackageNamePatterns.stream().anyMatch(pattern -> 
pattern.matcher(packageName).matches());
     }
 
     protected boolean isExcludedPackageNames(Class<?> clazz) {
diff --git 
a/core/src/test/java/org/apache/struts2/ognl/SecurityMemberAccessPackageMatchingTest.java
 
b/core/src/test/java/org/apache/struts2/ognl/SecurityMemberAccessPackageMatchingTest.java
index 3dfa540dd..d333b20af 100644
--- 
a/core/src/test/java/org/apache/struts2/ognl/SecurityMemberAccessPackageMatchingTest.java
+++ 
b/core/src/test/java/org/apache/struts2/ognl/SecurityMemberAccessPackageMatchingTest.java
@@ -53,6 +53,15 @@ public class SecurityMemberAccessPackageMatchingTest {
                 .anyMatch(matchingPackages::contains);
     }
 
+    /**
+     * The default-package condition replaced by WW-5677 in {@code 
checkDefaultPackageAccess},
+     * retained verbatim as the reference oracle. Deliberately calls {@link 
Class#getPackage()}
+     * directly rather than delegating to production code, so that it cannot 
drift with it.
+     */
+    private static boolean legacyDefaultPackageCondition(Class<?> clazz) {
+        return clazz.getPackage() == null || 
clazz.getPackage().getName().isEmpty();
+    }
+
     /**
      * The {@code toPackageName} implementation replaced by WW-5674, retained 
as the reference oracle.
      */
@@ -168,6 +177,21 @@ public class SecurityMemberAccessPackageMatchingTest {
         }
     }
 
+    /**
+     * WW-5677 replaced {@code getPackage() == null || 
getPackage().getName().isEmpty()} in
+     * {@code checkDefaultPackageAccess} with {@code 
toPackageName(clazz).isEmpty()}. That gate
+     * decides whether a class counts as living in the default package, so the 
equivalence is
+     * asserted against the frozen oracle over every class shape rather than 
argued.
+     */
+    @Test
+    public void defaultPackageConditionMatchesLegacyAcrossClassShapes() throws 
Exception {
+        for (Class<?> clazz : classShapes()) {
+            assertThat(toPackageName(clazz).isEmpty())
+                    .as("default-package condition for %s", clazz.getName())
+                    .isEqualTo(legacyDefaultPackageCondition(clazz));
+        }
+    }
+
     @Test
     public void arraysAndPrimitivesResolveToTheEmptyPackage() {
         assertThat(toPackageName(int.class)).isEmpty();
diff --git 
a/core/src/test/java/org/apache/struts2/ognl/SecurityMemberAccessTest.java 
b/core/src/test/java/org/apache/struts2/ognl/SecurityMemberAccessTest.java
index 033863631..4bfb3c226 100644
--- a/core/src/test/java/org/apache/struts2/ognl/SecurityMemberAccessTest.java
+++ b/core/src/test/java/org/apache/struts2/ognl/SecurityMemberAccessTest.java
@@ -404,6 +404,33 @@ public class SecurityMemberAccessTest {
         assertFalse("default package isn't excluded!", actual);
     }
 
+    /**
+     * WW-5677 routed {@code checkDefaultPackageAccess} through {@code 
toPackageName}. A class in a
+     * named package must still pass the gate when the setting is on.
+     */
+    @Test
+    public void testDefaultPackageAccessPermitsNamedPackageClass() throws 
Exception {
+        sma.useDisallowDefaultPackageAccess(Boolean.TRUE.toString());
+
+        Member member = FooBar.class.getMethod("getStringField");
+
+        assertTrue("a named-package class is blocked!", 
sma.checkDefaultPackageAccess(new FooBar(), member));
+    }
+
+    /**
+     * WW-5677 equivalence at the shape most likely to break it: an array 
target reports the empty
+     * package under both the old {@code getPackage() == null} form and the 
new one, so it must stay
+     * blocked. The member here declares in {@code java.lang}, so only the 
target branch can block.
+     */
+    @Test
+    public void testDefaultPackageAccessBlocksArrayTarget() throws Exception {
+        sma.useDisallowDefaultPackageAccess(Boolean.TRUE.toString());
+
+        Member member = Object.class.getMethod("toString");
+
+        assertFalse("an array target isn't blocked!", 
sma.checkDefaultPackageAccess(new String[0], member));
+    }
+
     @Test
     public void testDefaultPackageExclusion2() throws Exception {
         // given

Reply via email to