Merge pull request #736 from apache/WW-5337-exclusion-performance

WW-5337 Make SecurityMemberAccess exclusion checking more performant
This commit is contained in:
Kusal Kithul-Godage
2023-08-22 11:28:36 +10:00
committed by GitHub
4 changed files with 84 additions and 95 deletions
@@ -24,7 +24,6 @@ import com.opensymphony.xwork2.inject.Container;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.ognl.accessor.CompoundRootAccessor;
import com.opensymphony.xwork2.util.CompoundRoot;
import com.opensymphony.xwork2.util.TextParseUtil;
import com.opensymphony.xwork2.util.reflection.ReflectionException;
import ognl.ClassResolver;
import ognl.Ognl;
@@ -51,6 +50,11 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import static com.opensymphony.xwork2.util.TextParseUtil.commaDelimitedStringToSet;
import static java.util.stream.Collectors.toSet;
import static org.apache.commons.lang3.StringUtils.strip;
/**
@@ -187,9 +191,8 @@ public class OgnlUtil {
}
private Set<Class<?>> parseClasses(String commaDelimitedClasses) {
Set<String> classNames = TextParseUtil.commaDelimitedStringToSet(commaDelimitedClasses);
Set<String> classNames = commaDelimitedStringToSet(commaDelimitedClasses);
Set<Class<?>> classes = new HashSet<>();
for (String className : classNames) {
try {
classes.add(Class.forName(className));
@@ -197,7 +200,6 @@ public class OgnlUtil {
throw new ConfigurationException("Cannot load class for exclusion/exemption configuration: " + className, e);
}
}
return classes;
}
@@ -218,14 +220,13 @@ public class OgnlUtil {
}
private Set<Pattern> parseExcludedPackageNamePatterns(String commaDelimitedPackagePatterns) {
Set<String> packagePatterns = TextParseUtil.commaDelimitedStringToSet(commaDelimitedPackagePatterns);
Set<Pattern> packageNamePatterns = new HashSet<>();
for (String pattern : packagePatterns) {
packageNamePatterns.add(Pattern.compile(pattern));
try {
return commaDelimitedStringToSet(commaDelimitedPackagePatterns)
.stream().map(Pattern::compile).collect(toSet());
} catch (PatternSyntaxException e) {
throw new ConfigurationException(
"Excluded package name patterns could not be parsed due to invalid regex: " + commaDelimitedPackagePatterns, e);
}
return packageNamePatterns;
}
@Inject(value = StrutsConstants.STRUTS_EXCLUDED_PACKAGE_NAMES, required = false)
@@ -261,7 +262,8 @@ public class OgnlUtil {
}
private Set<String> parseExcludedPackageNames(String commaDelimitedPackageNames) {
Set<String> parsedSet = TextParseUtil.commaDelimitedStringToSet(commaDelimitedPackageNames);
Set<String> parsedSet = commaDelimitedStringToSet(commaDelimitedPackageNames)
.stream().map(s -> strip(s, ".")).collect(toSet());
if (parsedSet.stream().anyMatch(s -> s.matches("(.*?)\\s(.*?)"))) {
throw new ConfigurationException("Excluded package names could not be parsed due to erroneous whitespace characters: " + commaDelimitedPackageNames);
}
@@ -27,13 +27,17 @@ import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.util.Collections.emptySet;
import static java.util.Collections.unmodifiableSet;
/**
* Allows access decisions to be made on the basis of whether a member is static or not.
* Also blocks or allows access to properties.
@@ -43,12 +47,12 @@ public class SecurityMemberAccess implements MemberAccess {
private static final Logger LOG = LogManager.getLogger(SecurityMemberAccess.class);
private final boolean allowStaticFieldAccess;
private Set<Pattern> excludeProperties = Collections.emptySet();
private Set<Pattern> acceptProperties = Collections.emptySet();
private Set<Class<?>> excludedClasses = Collections.emptySet();
private Set<Pattern> excludedPackageNamePatterns = Collections.emptySet();
private Set<String> excludedPackageNames = Collections.emptySet();
private Set<Class<?>> excludedPackageExemptClasses = Collections.emptySet();
private Set<Pattern> excludeProperties = emptySet();
private Set<Pattern> acceptProperties = emptySet();
private Set<Class<?>> excludedClasses = emptySet();
private Set<Pattern> excludedPackageNamePatterns = emptySet();
private Set<String> excludedPackageNames = emptySet();
private Set<Class<?>> excludedPackageExemptClasses = emptySet();
private boolean disallowProxyMemberAccess;
/**
@@ -60,6 +64,7 @@ public class SecurityMemberAccess implements MemberAccess {
*/
public SecurityMemberAccess(boolean allowStaticFieldAccess) {
this.allowStaticFieldAccess = allowStaticFieldAccess;
useExcludedClasses(excludedClasses); // Initialise default exclusions
}
@Override
@@ -237,18 +242,15 @@ public class SecurityMemberAccess implements MemberAccess {
protected boolean isExcludedPackageNamePatterns(Class<?> clazz) {
String packageName = toPackageName(clazz);
for (Pattern pattern : excludedPackageNamePatterns) {
if (pattern.matcher(packageName).matches()) {
return true;
}
}
return false;
return excludedPackageNamePatterns.stream().anyMatch(pattern -> pattern.matcher(packageName).matches());
}
protected boolean isExcludedPackageNames(Class<?> clazz) {
String suffixedPackageName = toPackageName(clazz) + ".";
for (String excludedPackageName : excludedPackageNames) {
if (suffixedPackageName.startsWith(excludedPackageName)) {
String packageName = toPackageName(clazz);
List<String> packageParts = Arrays.asList(packageName.split("\\."));
for (int i = 0; i < packageParts.size(); i++) {
String parentPackage = String.join(".", packageParts.subList(0, i + 1));
if (excludedPackageNames.contains(parentPackage)) {
return true;
}
}
@@ -256,14 +258,11 @@ public class SecurityMemberAccess implements MemberAccess {
}
protected boolean isClassExcluded(Class<?> clazz) {
if (clazz == Object.class || (clazz == Class.class && !allowStaticFieldAccess)) {
return true;
}
return excludedClasses.stream().anyMatch(clazz::isAssignableFrom);
return excludedClasses.contains(clazz);
}
protected boolean isExcludedPackageExempt(Class<?> clazz) {
return excludedPackageExemptClasses.stream().anyMatch(clazz::equals);
return excludedPackageExemptClasses.contains(clazz);
}
protected boolean isAcceptableProperty(String name) {
@@ -328,11 +327,16 @@ public class SecurityMemberAccess implements MemberAccess {
*/
@Deprecated
public void setExcludedClasses(Set<Class<?>> excludedClasses) {
this.excludedClasses = excludedClasses;
useExcludedClasses(excludedClasses);
}
public void useExcludedClasses(Set<Class<?>> excludedClasses) {
this.excludedClasses = excludedClasses;
Set<Class<?>> newExcludedClasses = new HashSet<>(excludedClasses);
newExcludedClasses.add(Object.class);
if (!allowStaticFieldAccess) {
newExcludedClasses.add(Class.class);
}
this.excludedClasses = unmodifiableSet(newExcludedClasses);
}
/**
@@ -55,52 +55,52 @@
<!-- constant name="struts.excludedPackageNamePatterns" value="^java\.lang\..*,^ognl.*,^(?!javax\.servlet\..+)(javax\..+)" / -->
<!-- constant name="struts.devMode.excludedPackageNamePatterns" value="^java\.lang\..*,^ognl.*,^(?!javax\.servlet\..+)(javax\..+)" / -->
<!-- this is simpler version of the above used with string comparison -->
<!-- All classes within the following packages and their sub-packages are excluded -->
<constant name="struts.excludedPackageNames"
value="
ognl.,
java.io.,
java.net.,
java.nio.,
javax.,
freemarker.core.,
freemarker.template.,
freemarker.ext.jsp.,
freemarker.ext.rhino.,
sun.misc.,
sun.reflect.,
javassist.,
org.apache.velocity.,
org.objectweb.asm.,
org.springframework.context.,
com.opensymphony.xwork2.inject.,
com.opensymphony.xwork2.ognl.,
com.opensymphony.xwork2.security.,
com.opensymphony.xwork2.util.,
org.apache.tomcat.,
org.apache.catalina.core.,
org.wildfly.extension.undertow.deployment."/>
ognl,
java.io,
java.net,
java.nio,
javax,
freemarker.core,
freemarker.template,
freemarker.ext.jsp,
freemarker.ext.rhino,
sun.misc,
sun.reflect,
javassist,
org.apache.velocity,
org.objectweb.asm,
org.springframework.context,
com.opensymphony.xwork2.inject,
com.opensymphony.xwork2.ognl,
com.opensymphony.xwork2.security,
com.opensymphony.xwork2.util,
org.apache.tomcat,
org.apache.catalina.core,
org.wildfly.extension.undertow.deployment"/>
<constant name="struts.devMode.excludedPackageNames"
value="
ognl.,
java.io.,
java.net.,
java.nio.,
javax.,
freemarker.core.,
freemarker.template.,
freemarker.ext.jsp.,
freemarker.ext.rhino.,
sun.misc.,
sun.reflect.,
javassist.,
org.apache.velocity.,
org.objectweb.asm.,
org.springframework.context.,
com.opensymphony.xwork2.inject.,
com.opensymphony.xwork2.ognl.,
com.opensymphony.xwork2.security.,
com.opensymphony.xwork2.util."/>
ognl,
java.io,
java.net,
java.nio,
javax,
freemarker.core,
freemarker.template,
freemarker.ext.jsp,
freemarker.ext.rhino,
sun.misc,
sun.reflect,
javassist,
org.apache.velocity,
org.objectweb.asm,
org.springframework.context,
com.opensymphony.xwork2.inject,
com.opensymphony.xwork2.ognl,
com.opensymphony.xwork2.security,
com.opensymphony.xwork2.util"/>
</struts>
@@ -178,23 +178,6 @@ public class SecurityMemberAccessTest {
assertTrue("barLogic() from BarInterface isn't accessible!!!", accessible);
}
@Test
public void testMiddleOfInheritanceExclusion4() throws Exception {
// given
String propertyName = "barLogic";
Member member = BarInterface.class.getMethod(propertyName);
Set<Class<?>> excluded = new HashSet<>();
excluded.add(FooBarInterface.class);
sma.useExcludedClasses(excluded);
// when
boolean accessible = sma.isAccessible(context, target, member, propertyName);
// then
assertFalse("barLogic() from BarInterface is accessible!!!", accessible);
}
@Test
public void testPackageExclusion() throws Exception {
// given
@@ -790,7 +773,7 @@ public class SecurityMemberAccessTest {
@Test
public void testPackageNameExclusionAsCommaDelimited() {
// given
sma.useExcludedPackageNames(TextParseUtil.commaDelimitedStringToSet("java.lang."));
sma.useExcludedPackageNames(TextParseUtil.commaDelimitedStringToSet("java.lang"));
// when
boolean actual = sma.isPackageExcluded(String.class, String.class);