| /* |
| * 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.inject.Inject; |
| import com.opensymphony.xwork2.util.ProxyUtil; |
| import ognl.MemberAccess; |
| import org.apache.commons.lang3.BooleanUtils; |
| import org.apache.logging.log4j.LogManager; |
| import org.apache.logging.log4j.Logger; |
| import org.apache.struts2.StrutsConstants; |
| import org.apache.struts2.ognl.ProviderAllowlist; |
| import org.apache.struts2.ognl.ThreadAllowlist; |
| |
| import java.lang.reflect.AccessibleObject; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.Member; |
| import java.lang.reflect.Modifier; |
| 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 com.opensymphony.xwork2.util.ConfigParseUtil.toClassObjectsSet; |
| import static com.opensymphony.xwork2.util.ConfigParseUtil.toClassesSet; |
| import static com.opensymphony.xwork2.util.ConfigParseUtil.toNewClassesSet; |
| import static com.opensymphony.xwork2.util.ConfigParseUtil.toNewPackageNamesSet; |
| import static com.opensymphony.xwork2.util.ConfigParseUtil.toNewPatternsSet; |
| import static com.opensymphony.xwork2.util.ConfigParseUtil.toPackageNamesSet; |
| import static java.text.MessageFormat.format; |
| import static java.util.Collections.emptySet; |
| import static java.util.Collections.singletonList; |
| 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. |
| */ |
| public class SecurityMemberAccess implements MemberAccess { |
| |
| private static final Logger LOG = LogManager.getLogger(SecurityMemberAccess.class); |
| |
| private static final Set<String> ALLOWLIST_REQUIRED_PACKAGES = unmodifiableSet(new HashSet<>(Arrays.asList( |
| "com.opensymphony.xwork2.validator.validators", |
| "org.apache.struts2.components", |
| "org.apache.struts2.views.jsp" |
| ))); |
| |
| private static final Set<Class<?>> ALLOWLIST_REQUIRED_CLASSES = unmodifiableSet(new HashSet<>(Arrays.asList( |
| java.lang.Enum.class, |
| java.lang.String.class, |
| java.util.Date.class, |
| java.util.HashMap.class, |
| java.util.Map.class, |
| java.util.Map.Entry.class |
| ))); |
| |
| private final ProviderAllowlist providerAllowlist; |
| private final ThreadAllowlist threadAllowlist; |
| private boolean allowStaticFieldAccess = true; |
| private Set<Pattern> excludeProperties = emptySet(); |
| private Set<Pattern> acceptProperties = emptySet(); |
| private Set<String> excludedClasses = unmodifiableSet(new HashSet<>(singletonList(Object.class.getName()))); |
| private Set<Pattern> excludedPackageNamePatterns = emptySet(); |
| private Set<String> excludedPackageNames = emptySet(); |
| private Set<String> excludedPackageExemptClasses = emptySet(); |
| private boolean enforceAllowlistEnabled = false; |
| private Set<Class<?>> allowlistClasses = emptySet(); |
| private Set<String> allowlistPackageNames = emptySet(); |
| private boolean disallowProxyObjectAccess = false; |
| private boolean disallowProxyMemberAccess = false; |
| private boolean disallowDefaultPackageAccess = false; |
| |
| @Inject |
| public SecurityMemberAccess(@Inject ProviderAllowlist providerAllowlist, @Inject ThreadAllowlist threadAllowlist) { |
| this.providerAllowlist = providerAllowlist; |
| this.threadAllowlist = threadAllowlist; |
| } |
| |
| /** |
| * SecurityMemberAccess |
| * - access decisions based on whether member is static (or not) |
| * - block or allow access to properties (configurable-after-construction) |
| * |
| * @param allowStaticFieldAccess if set to true static fields (constants) will be accessible |
| * @deprecated since 6.4.0, use {@link #SecurityMemberAccess(ProviderAllowlist, ThreadAllowlist)} instead. |
| */ |
| @Deprecated |
| public SecurityMemberAccess(boolean allowStaticFieldAccess) { |
| this(null, null); |
| useAllowStaticFieldAccess(String.valueOf(allowStaticFieldAccess)); |
| } |
| |
| @Override |
| public Object setup(Map context, Object target, Member member, String propertyName) { |
| Object result = null; |
| |
| if (isAccessible(context, target, member, propertyName)) { |
| final AccessibleObject accessible = (AccessibleObject) member; |
| |
| if (!accessible.isAccessible()) { |
| result = Boolean.FALSE; |
| accessible.setAccessible(true); |
| } |
| } |
| return result; |
| } |
| |
| @Override |
| public void restore(Map context, Object target, Member member, String propertyName, Object state) { |
| if (state == null) { |
| return; |
| } |
| if ((Boolean) state) { |
| throw new IllegalArgumentException(format( |
| "Improper restore state [true] for target [{0}], member [{1}], propertyName [{2}]", |
| target, |
| member, |
| propertyName)); |
| } |
| ((AccessibleObject) member).setAccessible(false); |
| } |
| |
| @Override |
| public boolean isAccessible(Map context, Object target, Member member, String propertyName) { |
| LOG.debug("Checking access for [target: {}, member: {}, property: {}]", target, member, propertyName); |
| |
| if (target != null) { |
| // Special case: Target is a Class object but not Class.class |
| if (Class.class.equals(target.getClass()) && !Class.class.equals(target)) { |
| if (!isStatic(member)) { |
| throw new IllegalArgumentException("Member expected to be static!"); |
| } |
| if (!member.getDeclaringClass().equals(target)) { |
| throw new IllegalArgumentException("Target class does not match static member!"); |
| } |
| target = null; // This information is not useful to us and conflicts with following logic which expects target to be null or an instance containing the member |
| // Standard case: Member should exist on target |
| } else if (!member.getDeclaringClass().isAssignableFrom(target.getClass())) { |
| throw new IllegalArgumentException("Member does not exist on target!"); |
| } |
| } |
| |
| if (!checkProxyObjectAccess(target)) { |
| LOG.warn("Access to proxy is blocked! Target [{}], proxy class [{}]", target, target.getClass().getName()); |
| return false; |
| } |
| |
| if (!checkProxyMemberAccess(target, member)) { |
| LOG.warn("Access to proxy is blocked! Member class [{}] of target [{}], member [{}]", member.getDeclaringClass(), target, member); |
| return false; |
| } |
| |
| if (!checkPublicMemberAccess(member)) { |
| LOG.warn("Access to non-public [{}] is blocked!", member); |
| return false; |
| } |
| |
| if (!checkStaticFieldAccess(member)) { |
| LOG.warn("Access to static field [{}] is blocked!", member); |
| return false; |
| } |
| |
| if (!checkStaticMethodAccess(member)) { |
| LOG.warn("Access to static method [{}] is blocked!", member); |
| return false; |
| } |
| |
| if (!checkDefaultPackageAccess(target, member)) { |
| return false; |
| } |
| |
| if (!checkExclusionList(target, member)) { |
| return false; |
| } |
| |
| if (!checkAllowlist(target, member)) { |
| return false; |
| } |
| |
| if (!isAcceptableProperty(propertyName)) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /** |
| * @return {@code true} if member access is allowed |
| */ |
| protected boolean checkAllowlist(Object target, Member member) { |
| Class<?> memberClass = member.getDeclaringClass(); |
| if (!enforceAllowlistEnabled) { |
| return true; |
| } |
| if (!isClassAllowlisted(memberClass)) { |
| LOG.warn(format("Declaring class [{0}] of member type [{1}] is not allowlisted!", memberClass, member)); |
| return false; |
| } |
| if (target == null || target.getClass() == memberClass) { |
| return true; |
| } |
| Class<?> targetClass = target.getClass(); |
| if (!isClassAllowlisted(targetClass)) { |
| LOG.warn(format("Target class [{0}] of target [{1}] is not allowlisted!", targetClass, target)); |
| return false; |
| } |
| return true; |
| } |
| |
| protected boolean isClassAllowlisted(Class<?> clazz) { |
| return allowlistClasses.contains(clazz) |
| || ALLOWLIST_REQUIRED_CLASSES.contains(clazz) |
| || (providerAllowlist != null && providerAllowlist.getProviderAllowlist().contains(clazz)) |
| || (threadAllowlist != null && threadAllowlist.getAllowlist().contains(clazz)) |
| || isClassBelongsToPackages(clazz, ALLOWLIST_REQUIRED_PACKAGES) |
| || isClassBelongsToPackages(clazz, allowlistPackageNames); |
| } |
| |
| /** |
| * @return {@code true} if member access is allowed |
| */ |
| protected boolean checkExclusionList(Object target, Member member) { |
| Class<?> memberClass = member.getDeclaringClass(); |
| if (isClassExcluded(memberClass)) { |
| LOG.warn("Declaring class of member type [{}] is excluded!", memberClass); |
| return false; |
| } |
| if (isPackageExcluded(memberClass)) { |
| LOG.warn("Package [{}] of member class [{}] of member [{}] is excluded!", |
| memberClass.getPackage(), |
| memberClass, |
| target); |
| return false; |
| } |
| if (target == null || target.getClass() == memberClass) { |
| return true; |
| } |
| Class<?> targetClass = target.getClass(); |
| if (isClassExcluded(targetClass)) { |
| LOG.warn("Target class [{}] of target [{}] is excluded!", targetClass, target); |
| return false; |
| } |
| if (isPackageExcluded(targetClass)) { |
| LOG.warn("Package [{}] of target [{}] is excluded!", targetClass.getPackage(), member); |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * @return {@code true} if member access is allowed |
| */ |
| protected boolean checkDefaultPackageAccess(Object target, Member member) { |
| if (!disallowDefaultPackageAccess) { |
| return true; |
| } |
| Class<?> memberClass = member.getDeclaringClass(); |
| if (memberClass.getPackage() == null || memberClass.getPackage().getName().isEmpty()) { |
| LOG.warn("Class [{}] from the default package is excluded!", memberClass); |
| return false; |
| } |
| if (target == null || target.getClass() == memberClass) { |
| return true; |
| } |
| Class<?> targetClass = target.getClass(); |
| if (targetClass.getPackage() == null || targetClass.getPackage().getName().isEmpty()) { |
| LOG.warn("Class [{}] from the default package is excluded!", targetClass); |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * @return {@code true} if proxy object access is allowed |
| */ |
| protected boolean checkProxyObjectAccess(Object target) { |
| return !(disallowProxyObjectAccess && ProxyUtil.isProxy(target)); |
| } |
| |
| /** |
| * @return {@code true} if proxy member access is allowed |
| */ |
| protected boolean checkProxyMemberAccess(Object target, Member member) { |
| return !(disallowProxyMemberAccess && ProxyUtil.isProxyMember(member, target)); |
| } |
| |
| /** |
| * Check access for static method (via modifiers). |
| * <p> |
| * Note: For non-static members, the result is always true. |
| * |
| * @return {@code true} if member access is allowed |
| */ |
| protected boolean checkStaticMethodAccess(Member member) { |
| return member instanceof Field || !isStatic(member); |
| } |
| |
| private static boolean isStatic(Member member) { |
| return Modifier.isStatic(member.getModifiers()); |
| } |
| |
| /** |
| * Check access for static field (via modifiers). |
| * <p> |
| * Note: For non-static members, the result is always true. |
| * |
| * @return {@code true} if member access is allowed |
| */ |
| protected boolean checkStaticFieldAccess(Member member) { |
| if (allowStaticFieldAccess) { |
| return true; |
| } |
| return !(member instanceof Field) || !isStatic(member); |
| } |
| |
| /** |
| * Check access for public members (via modifiers) |
| * |
| * @return {@code true} if member access is allowed |
| */ |
| protected boolean checkPublicMemberAccess(Member member) { |
| return Modifier.isPublic(member.getModifiers()); |
| } |
| |
| protected boolean isPackageExcluded(Class<?> clazz) { |
| return !excludedPackageExemptClasses.contains(clazz.getName()) && (isExcludedPackageNames(clazz) || isExcludedPackageNamePatterns(clazz)); |
| } |
| |
| public static String toPackageName(Class<?> clazz) { |
| if (clazz.getPackage() == null) { |
| return ""; |
| } |
| return clazz.getPackage().getName(); |
| } |
| |
| protected boolean isExcludedPackageNamePatterns(Class<?> clazz) { |
| return excludedPackageNamePatterns.stream().anyMatch(pattern -> pattern.matcher(toPackageName(clazz)).matches()); |
| } |
| |
| protected boolean isExcludedPackageNames(Class<?> clazz) { |
| return isClassBelongsToPackages(clazz, excludedPackageNames); |
| } |
| |
| public static boolean isClassBelongsToPackages(Class<?> clazz, Set<String> matchingPackages) { |
| List<String> packageParts = Arrays.asList(toPackageName(clazz).split("\\.")); |
| for (int i = 0; i < packageParts.size(); i++) { |
| String parentPackage = String.join(".", packageParts.subList(0, i + 1)); |
| if (matchingPackages.contains(parentPackage)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| protected boolean isClassExcluded(Class<?> clazz) { |
| return excludedClasses.contains(clazz.getName()); |
| } |
| |
| /** |
| * @return {@code true} if member access is allowed |
| */ |
| protected boolean isAcceptableProperty(String name) { |
| return name == null || !isExcluded(name) && isAccepted(name); |
| } |
| |
| protected boolean isAccepted(String paramName) { |
| if (acceptProperties.isEmpty()) { |
| return true; |
| } |
| return acceptProperties.stream().map(pattern -> pattern.matcher(paramName)).anyMatch(Matcher::matches); |
| } |
| |
| protected boolean isExcluded(String paramName) { |
| return excludeProperties.stream().map(pattern -> pattern.matcher(paramName)).anyMatch(Matcher::matches); |
| } |
| |
| public void useExcludeProperties(Set<Pattern> excludeProperties) { |
| this.excludeProperties = excludeProperties; |
| } |
| |
| public void useAcceptProperties(Set<Pattern> acceptedProperties) { |
| this.acceptProperties = acceptedProperties; |
| } |
| |
| @Inject(value = StrutsConstants.STRUTS_ALLOW_STATIC_FIELD_ACCESS, required = false) |
| public void useAllowStaticFieldAccess(String allowStaticFieldAccess) { |
| this.allowStaticFieldAccess = BooleanUtils.toBoolean(allowStaticFieldAccess); |
| if (!this.allowStaticFieldAccess) { |
| useExcludedClasses(Class.class.getName()); |
| } |
| } |
| |
| @Inject(value = StrutsConstants.STRUTS_EXCLUDED_CLASSES, required = false) |
| public void useExcludedClasses(String commaDelimitedClasses) { |
| this.excludedClasses = toNewClassesSet(excludedClasses, commaDelimitedClasses); |
| } |
| |
| @Inject(value = StrutsConstants.STRUTS_EXCLUDED_PACKAGE_NAME_PATTERNS, required = false) |
| public void useExcludedPackageNamePatterns(String commaDelimitedPackagePatterns) { |
| this.excludedPackageNamePatterns = toNewPatternsSet(excludedPackageNamePatterns, commaDelimitedPackagePatterns); |
| } |
| |
| @Inject(value = StrutsConstants.STRUTS_EXCLUDED_PACKAGE_NAMES, required = false) |
| public void useExcludedPackageNames(String commaDelimitedPackageNames) { |
| this.excludedPackageNames = toNewPackageNamesSet(excludedPackageNames, commaDelimitedPackageNames); |
| } |
| |
| @Inject(value = StrutsConstants.STRUTS_EXCLUDED_PACKAGE_EXEMPT_CLASSES, required = false) |
| public void useExcludedPackageExemptClasses(String commaDelimitedClasses) { |
| this.excludedPackageExemptClasses = toClassesSet(commaDelimitedClasses); |
| } |
| |
| @Inject(value = StrutsConstants.STRUTS_ALLOWLIST_ENABLE, required = false) |
| public void useEnforceAllowlistEnabled(String enforceAllowlistEnabled) { |
| this.enforceAllowlistEnabled = BooleanUtils.toBoolean(enforceAllowlistEnabled); |
| } |
| |
| @Inject(value = StrutsConstants.STRUTS_ALLOWLIST_CLASSES, required = false) |
| public void useAllowlistClasses(String commaDelimitedClasses) { |
| this.allowlistClasses = toClassObjectsSet(commaDelimitedClasses); |
| } |
| |
| @Inject(value = StrutsConstants.STRUTS_ALLOWLIST_PACKAGE_NAMES, required = false) |
| public void useAllowlistPackageNames(String commaDelimitedPackageNames) { |
| this.allowlistPackageNames = toPackageNamesSet(commaDelimitedPackageNames); |
| } |
| |
| @Inject(value = StrutsConstants.STRUTS_DISALLOW_PROXY_OBJECT_ACCESS, required = false) |
| public void useDisallowProxyObjectAccess(String disallowProxyObjectAccess) { |
| this.disallowProxyObjectAccess = BooleanUtils.toBoolean(disallowProxyObjectAccess); |
| } |
| |
| @Inject(value = StrutsConstants.STRUTS_DISALLOW_PROXY_MEMBER_ACCESS, required = false) |
| public void useDisallowProxyMemberAccess(String disallowProxyMemberAccess) { |
| this.disallowProxyMemberAccess = BooleanUtils.toBoolean(disallowProxyMemberAccess); |
| } |
| |
| @Inject(value = StrutsConstants.STRUTS_DISALLOW_DEFAULT_PACKAGE_ACCESS, required = false) |
| public void useDisallowDefaultPackageAccess(String disallowDefaultPackageAccess) { |
| this.disallowDefaultPackageAccess = BooleanUtils.toBoolean(disallowDefaultPackageAccess); |
| } |
| } |