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 0e22e74ce..e41004a2b 100644 --- a/core/src/main/java/org/apache/struts2/ognl/SecurityMemberAccess.java +++ b/core/src/main/java/org/apache/struts2/ognl/SecurityMemberAccess.java @@ -32,11 +32,9 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Member; import java.lang.reflect.Modifier; -import java.util.List; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.IntStream; import static java.text.MessageFormat.format; import static java.util.Collections.emptySet; @@ -393,10 +391,33 @@ public class SecurityMemberAccess implements MemberAccess { } public static boolean isClassBelongsToPackages(Class clazz, Set matchingPackages) { - List packageParts = List.of(toPackageName(clazz).split("\\.")); - return IntStream.range(0, packageParts.size()) - .mapToObj(i -> String.join(".", packageParts.subList(0, i + 1))) - .anyMatch(matchingPackages::contains); + return isPackageBelongsToPackages(toPackageName(clazz), matchingPackages, emptySet()); + } + + /** + * Tests whether the given package name, or any of its parent packages, is present in either + * set. Walks the name in place rather than building the full prefix list, since this runs on + * the OGNL member-access path. Shortest prefix first, so broad entries such as {@code java.io} + * short-circuit earliest. + * + * @param packageName the package name to test, empty for the default package + * @param first the first set of package names to match against + * @param second the second set of package names to match against + * @return {@code true} if the package or any parent package is in either set + */ + static boolean isPackageBelongsToPackages(String packageName, Set first, Set second) { + if (first.isEmpty() && second.isEmpty()) { + return false; + } + int idx = packageName.indexOf('.'); + while (idx != -1) { + String prefix = packageName.substring(0, idx); + if (first.contains(prefix) || second.contains(prefix)) { + return true; + } + idx = packageName.indexOf('.', idx + 1); + } + return first.contains(packageName) || second.contains(packageName); } protected boolean isClassExcluded(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 9f8af5556..04e2da96c 100644 --- a/core/src/test/java/org/apache/struts2/ognl/SecurityMemberAccessPackageMatchingTest.java +++ b/core/src/test/java/org/apache/struts2/ognl/SecurityMemberAccessPackageMatchingTest.java @@ -175,4 +175,24 @@ public class SecurityMemberAccessPackageMatchingTest { } } } + + @Test + public void indexWalkMatchesLegacyAcrossPackageNameShapes() { + for (String packageName : PACKAGE_NAMES) { + for (Set candidates : CANDIDATE_SETS) { + assertThat(SecurityMemberAccess.isPackageBelongsToPackages(packageName, candidates, emptySet())) + .as("packageName=[%s] candidates=%s", packageName, candidates) + .isEqualTo(legacyPrefixMatch(packageName, candidates)); + } + } + } + + @Test + public void bothSetsEmptyShortCircuitsToFalse() { + for (String packageName : PACKAGE_NAMES) { + assertThat(SecurityMemberAccess.isPackageBelongsToPackages(packageName, emptySet(), emptySet())) + .as("packageName=[%s] with no configured packages", packageName) + .isFalse(); + } + } }