This is an automated email from the ASF dual-hosted git repository. kusal pushed a commit to branch WW-5340-ognl-guard in repository https://gitbox.apache.org/repos/asf/struts.git
commit 7a9c61cb69a8ad1f7b6e07fdeb278ee704119ca2 Author: Kusal Kithul-Godage <g...@kusal.io> AuthorDate: Thu Aug 31 19:21:51 2023 +1000 WW-5340 Introducing OGNL Guard --- .../xwork2/config/impl/DefaultConfiguration.java | 85 +++++++++++++++++++--- .../StrutsDefaultConfigurationProvider.java | 3 + .../opensymphony/xwork2/ognl/DefaultOgnlGuard.java | 79 ++++++++++++++++++++ .../com/opensymphony/xwork2/ognl/OgnlGuard.java | 37 ++++++++++ .../com/opensymphony/xwork2/ognl/OgnlUtil.java | 42 +++++------ .../java/org/apache/struts2/StrutsConstants.java | 3 + core/src/main/resources/struts-beans.xml | 1 + 7 files changed, 217 insertions(+), 33 deletions(-) diff --git a/core/src/main/java/com/opensymphony/xwork2/config/impl/DefaultConfiguration.java b/core/src/main/java/com/opensymphony/xwork2/config/impl/DefaultConfiguration.java index 71fdf2ff8..92852657e 100644 --- a/core/src/main/java/com/opensymphony/xwork2/config/impl/DefaultConfiguration.java +++ b/core/src/main/java/com/opensymphony/xwork2/config/impl/DefaultConfiguration.java @@ -18,25 +18,82 @@ */ package com.opensymphony.xwork2.config.impl; -import com.opensymphony.xwork2.*; -import com.opensymphony.xwork2.config.*; -import com.opensymphony.xwork2.config.entities.*; +import com.opensymphony.xwork2.ActionContext; +import com.opensymphony.xwork2.DefaultLocaleProviderFactory; +import com.opensymphony.xwork2.DefaultTextProvider; +import com.opensymphony.xwork2.FileManager; +import com.opensymphony.xwork2.FileManagerFactory; +import com.opensymphony.xwork2.LocaleProviderFactory; +import com.opensymphony.xwork2.LocalizedTextProvider; +import com.opensymphony.xwork2.ObjectFactory; +import com.opensymphony.xwork2.StrutsTextProviderFactory; +import com.opensymphony.xwork2.TextProvider; +import com.opensymphony.xwork2.TextProviderFactory; +import com.opensymphony.xwork2.config.Configuration; +import com.opensymphony.xwork2.config.ConfigurationException; +import com.opensymphony.xwork2.config.ContainerProvider; +import com.opensymphony.xwork2.config.FileManagerFactoryProvider; +import com.opensymphony.xwork2.config.FileManagerProvider; +import com.opensymphony.xwork2.config.PackageProvider; +import com.opensymphony.xwork2.config.RuntimeConfiguration; +import com.opensymphony.xwork2.config.entities.ActionConfig; +import com.opensymphony.xwork2.config.entities.InterceptorMapping; +import com.opensymphony.xwork2.config.entities.PackageConfig; +import com.opensymphony.xwork2.config.entities.ResultConfig; +import com.opensymphony.xwork2.config.entities.ResultTypeConfig; +import com.opensymphony.xwork2.config.entities.UnknownHandlerConfig; import com.opensymphony.xwork2.config.providers.EnvsValueSubstitutor; import com.opensymphony.xwork2.config.providers.InterceptorBuilder; import com.opensymphony.xwork2.config.providers.ValueSubstitutor; -import com.opensymphony.xwork2.conversion.*; -import com.opensymphony.xwork2.conversion.impl.*; -import com.opensymphony.xwork2.factory.*; -import com.opensymphony.xwork2.inject.*; +import com.opensymphony.xwork2.conversion.ConversionAnnotationProcessor; +import com.opensymphony.xwork2.conversion.ConversionFileProcessor; +import com.opensymphony.xwork2.conversion.ConversionPropertiesProcessor; +import com.opensymphony.xwork2.conversion.ObjectTypeDeterminer; +import com.opensymphony.xwork2.conversion.TypeConverter; +import com.opensymphony.xwork2.conversion.TypeConverterCreator; +import com.opensymphony.xwork2.conversion.TypeConverterHolder; +import com.opensymphony.xwork2.conversion.impl.ArrayConverter; +import com.opensymphony.xwork2.conversion.impl.CollectionConverter; +import com.opensymphony.xwork2.conversion.impl.DateConverter; +import com.opensymphony.xwork2.conversion.impl.DefaultConversionAnnotationProcessor; +import com.opensymphony.xwork2.conversion.impl.DefaultConversionFileProcessor; +import com.opensymphony.xwork2.conversion.impl.DefaultObjectTypeDeterminer; +import com.opensymphony.xwork2.conversion.impl.NumberConverter; +import com.opensymphony.xwork2.conversion.impl.StringConverter; +import com.opensymphony.xwork2.conversion.impl.XWorkBasicConverter; +import com.opensymphony.xwork2.conversion.impl.XWorkConverter; +import com.opensymphony.xwork2.factory.ActionFactory; +import com.opensymphony.xwork2.factory.ConverterFactory; +import com.opensymphony.xwork2.factory.DefaultActionFactory; +import com.opensymphony.xwork2.factory.DefaultInterceptorFactory; +import com.opensymphony.xwork2.factory.DefaultResultFactory; +import com.opensymphony.xwork2.factory.DefaultUnknownHandlerFactory; +import com.opensymphony.xwork2.factory.InterceptorFactory; +import com.opensymphony.xwork2.factory.ResultFactory; +import com.opensymphony.xwork2.factory.StrutsConverterFactory; +import com.opensymphony.xwork2.factory.UnknownHandlerFactory; +import com.opensymphony.xwork2.inject.Container; +import com.opensymphony.xwork2.inject.ContainerBuilder; +import com.opensymphony.xwork2.inject.Context; +import com.opensymphony.xwork2.inject.Factory; +import com.opensymphony.xwork2.inject.Scope; import com.opensymphony.xwork2.ognl.BeanInfoCacheFactory; import com.opensymphony.xwork2.ognl.DefaultOgnlBeanInfoCacheFactory; import com.opensymphony.xwork2.ognl.DefaultOgnlExpressionCacheFactory; +import com.opensymphony.xwork2.ognl.DefaultOgnlGuard; import com.opensymphony.xwork2.ognl.ExpressionCacheFactory; +import com.opensymphony.xwork2.ognl.OgnlGuard; import com.opensymphony.xwork2.ognl.OgnlReflectionProvider; import com.opensymphony.xwork2.ognl.OgnlUtil; import com.opensymphony.xwork2.ognl.OgnlValueStackFactory; import com.opensymphony.xwork2.ognl.accessor.CompoundRootAccessor; -import com.opensymphony.xwork2.util.*; +import com.opensymphony.xwork2.util.CompoundRoot; +import com.opensymphony.xwork2.util.OgnlTextParser; +import com.opensymphony.xwork2.util.PatternMatcher; +import com.opensymphony.xwork2.util.StrutsLocalizedTextProvider; +import com.opensymphony.xwork2.util.TextParser; +import com.opensymphony.xwork2.util.ValueStack; +import com.opensymphony.xwork2.util.ValueStackFactory; import com.opensymphony.xwork2.util.fs.DefaultFileManager; import com.opensymphony.xwork2.util.fs.DefaultFileManagerFactory; import com.opensymphony.xwork2.util.location.LocatableProperties; @@ -47,10 +104,17 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.struts2.StrutsConstants; import org.apache.struts2.conversion.StrutsConversionPropertiesProcessor; -import org.apache.struts2.conversion.StrutsTypeConverterHolder; import org.apache.struts2.conversion.StrutsTypeConverterCreator; +import org.apache.struts2.conversion.StrutsTypeConverterHolder; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; /** @@ -301,6 +365,7 @@ public class DefaultConfiguration implements Configuration { builder.factory(ExpressionCacheFactory.class, DefaultOgnlExpressionCacheFactory.class, Scope.SINGLETON); builder.factory(BeanInfoCacheFactory.class, DefaultOgnlBeanInfoCacheFactory.class, Scope.SINGLETON); builder.factory(OgnlUtil.class, Scope.SINGLETON); + builder.factory(OgnlGuard.class, DefaultOgnlGuard.class, Scope.SINGLETON); builder.factory(ValueSubstitutor.class, EnvsValueSubstitutor.class, Scope.SINGLETON); diff --git a/core/src/main/java/com/opensymphony/xwork2/config/providers/StrutsDefaultConfigurationProvider.java b/core/src/main/java/com/opensymphony/xwork2/config/providers/StrutsDefaultConfigurationProvider.java index 2c304ec82..2f9da8892 100644 --- a/core/src/main/java/com/opensymphony/xwork2/config/providers/StrutsDefaultConfigurationProvider.java +++ b/core/src/main/java/com/opensymphony/xwork2/config/providers/StrutsDefaultConfigurationProvider.java @@ -68,8 +68,10 @@ import com.opensymphony.xwork2.inject.Scope; import com.opensymphony.xwork2.ognl.BeanInfoCacheFactory; import com.opensymphony.xwork2.ognl.DefaultOgnlBeanInfoCacheFactory; import com.opensymphony.xwork2.ognl.DefaultOgnlExpressionCacheFactory; +import com.opensymphony.xwork2.ognl.DefaultOgnlGuard; import com.opensymphony.xwork2.ognl.ExpressionCacheFactory; import com.opensymphony.xwork2.ognl.ObjectProxy; +import com.opensymphony.xwork2.ognl.OgnlGuard; import com.opensymphony.xwork2.ognl.OgnlReflectionContextFactory; import com.opensymphony.xwork2.ognl.OgnlReflectionProvider; import com.opensymphony.xwork2.ognl.OgnlUtil; @@ -227,6 +229,7 @@ public class StrutsDefaultConfigurationProvider implements ConfigurationProvider .factory(ExpressionCacheFactory.class, DefaultOgnlExpressionCacheFactory.class, Scope.SINGLETON) .factory(BeanInfoCacheFactory.class, DefaultOgnlBeanInfoCacheFactory.class, Scope.SINGLETON) .factory(OgnlUtil.class, Scope.SINGLETON) + .factory(OgnlGuard.class, DefaultOgnlGuard.class, Scope.SINGLETON) .factory(CollectionConverter.class, Scope.SINGLETON) .factory(ArrayConverter.class, Scope.SINGLETON) .factory(DateConverter.class, Scope.SINGLETON) diff --git a/core/src/main/java/com/opensymphony/xwork2/ognl/DefaultOgnlGuard.java b/core/src/main/java/com/opensymphony/xwork2/ognl/DefaultOgnlGuard.java new file mode 100644 index 000000000..0d70d515c --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/ognl/DefaultOgnlGuard.java @@ -0,0 +1,79 @@ +/* + * 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 ognl.Node; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.struts2.StrutsConstants; + +import java.util.HashSet; +import java.util.Set; + +import static com.opensymphony.xwork2.util.TextParseUtil.commaDelimitedStringToSet; +import static java.util.Collections.emptySet; +import static java.util.Collections.unmodifiableSet; + +/** + * The default implementation of {@link OgnlGuard}. + * + * @since 6.4.0 + */ +public class DefaultOgnlGuard implements OgnlGuard { + + private static final Logger LOG = LogManager.getLogger(DefaultOgnlGuard.class); + + private Set<String> excludedNodeTypes = emptySet(); + + @Inject(value = StrutsConstants.STRUTS_OGNL_EXCLUDED_NODE_TYPES, required = false) + public void useExcludedNodeTypes(String excludedNodeTypes) { + Set<String> incomingExcludedNodeTypes = commaDelimitedStringToSet(excludedNodeTypes); + Set<String> newExcludeNodeTypes = new HashSet<>(this.excludedNodeTypes); + newExcludeNodeTypes.addAll(incomingExcludedNodeTypes); + this.excludedNodeTypes = unmodifiableSet(newExcludeNodeTypes); + } + + @Override + public boolean isBlocked(String expr, Object tree) { + return containsExcludedNodeType(tree); + } + + protected boolean containsExcludedNodeType(Object tree) { + if (!(tree instanceof Node) || excludedNodeTypes.isEmpty()) { + return false; + } + return recurseExcludedNodeType((Node) tree); + } + + protected boolean recurseExcludedNodeType(Node node) { + String nodeClassName = node.getClass().getName(); + if (excludedNodeTypes.contains(nodeClassName)) { + LOG.warn("Expression contains blocked node type [{}]", nodeClassName); + return true; + } else { + for (int i = 0; i < node.jjtGetNumChildren(); i++) { + if (containsExcludedNodeType(node.jjtGetChild(i))) { + return true; + } + } + return false; + } + } +} diff --git a/core/src/main/java/com/opensymphony/xwork2/ognl/OgnlGuard.java b/core/src/main/java/com/opensymphony/xwork2/ognl/OgnlGuard.java new file mode 100644 index 000000000..b1bcb409e --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/ognl/OgnlGuard.java @@ -0,0 +1,37 @@ +/* + * 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; + +/** + * Guards all expressions parsed by Struts Core. It is evaluated by {@link OgnlUtil} immediately after parsing any + * expression. + * + * @since 6.4.0 + */ +public interface OgnlGuard { + + /** + * It is imperative that the parsed tree matches the expression. + * + * @param expr OGNL expression + * @param tree parsed tree of expression + * @return whether the expression should be blocked + */ + boolean isBlocked(String expr, Object tree); +} diff --git a/core/src/main/java/com/opensymphony/xwork2/ognl/OgnlUtil.java b/core/src/main/java/com/opensymphony/xwork2/ognl/OgnlUtil.java index 8edac0a95..c080500aa 100644 --- a/core/src/main/java/com/opensymphony/xwork2/ognl/OgnlUtil.java +++ b/core/src/main/java/com/opensymphony/xwork2/ognl/OgnlUtil.java @@ -53,6 +53,7 @@ import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import static com.opensymphony.xwork2.util.TextParseUtil.commaDelimitedStringToSet; +import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.toSet; import static org.apache.commons.lang3.StringUtils.strip; @@ -73,6 +74,7 @@ public class OgnlUtil { private final OgnlCache<String, Object> expressionCache; private final OgnlCache<Class<?>, BeanInfo> beanInfoCache; private TypeConverter defaultConverter; + private final OgnlGuard ognlGuard; private boolean devMode; private boolean enableExpressionCache = true; @@ -95,37 +97,27 @@ public class OgnlUtil { /** * Construct a new OgnlUtil instance for use with the framework * - * @deprecated It is recommended to utilize the {@link OgnlUtil#OgnlUtil(com.opensymphony.xwork2.ognl.ExpressionCacheFactory, com.opensymphony.xwork2.ognl.BeanInfoCacheFactory) method instead. + * @deprecated since 6.0.0. Use {@link #OgnlUtil(ExpressionCacheFactory, BeanInfoCacheFactory, OgnlGuard) instead. */ @Deprecated public OgnlUtil() { - // Instantiate default Expression and BeanInfo caches (factories must be non-null). this(new DefaultOgnlExpressionCacheFactory<>(), - new DefaultOgnlBeanInfoCacheFactory<>()); + new DefaultOgnlBeanInfoCacheFactory<>(), + new DefaultOgnlGuard()); } /** - * Construct a new OgnlUtil instance for use with the framework, with optional - * cache factories for OGNL Expression and BeanInfo caches. + * Construct a new OgnlUtil instance for use with the framework, with optional cache factories for OGNL Expression + * and BeanInfo caches. * - * NOTE: Although the extension points are defined for the optional cache factories, developer-defined overrides do - * do not appear to function at this time (it always appears to instantiate the default factories). - * Construction injectors do not allow the optional flag, so the definitions must be defined. - * - * @param ognlExpressionCacheFactory factory for Expression cache instance. If null, it uses a default - * @param ognlBeanInfoCacheFactory factory for BeanInfo cache instance. If null, it uses a default + * @param ognlExpressionCacheFactory factory for Expression cache instance + * @param ognlBeanInfoCacheFactory factory for BeanInfo cache instance + * @param ognlGuard OGNL Guard instance */ @Inject - public OgnlUtil( - @Inject ExpressionCacheFactory<String, Object> ognlExpressionCacheFactory, - @Inject BeanInfoCacheFactory<Class<?>, BeanInfo> ognlBeanInfoCacheFactory - ) { - if (ognlExpressionCacheFactory == null) { - throw new IllegalArgumentException("ExpressionCacheFactory parameter cannot be null"); - } - if (ognlBeanInfoCacheFactory == null) { - throw new IllegalArgumentException("BeanInfoCacheFactory parameter cannot be null"); - } + public OgnlUtil(@Inject ExpressionCacheFactory<String, Object> ognlExpressionCacheFactory, + @Inject BeanInfoCacheFactory<Class<?>, BeanInfo> ognlBeanInfoCacheFactory, + @Inject OgnlGuard ognlGuard) { excludedClasses = Collections.unmodifiableSet(new HashSet<>()); excludedPackageNamePatterns = Collections.unmodifiableSet(new HashSet<>()); excludedPackageNames = Collections.unmodifiableSet(new HashSet<>()); @@ -136,8 +128,9 @@ public class OgnlUtil { devModeExcludedPackageNames = Collections.unmodifiableSet(new HashSet<>()); devModeExcludedPackageExemptClasses = Collections.unmodifiableSet(new HashSet<>()); - this.expressionCache = ognlExpressionCacheFactory.buildOgnlCache(); - this.beanInfoCache = ognlBeanInfoCacheFactory.buildOgnlCache(); + this.expressionCache = requireNonNull(ognlExpressionCacheFactory).buildOgnlCache(); + this.beanInfoCache = requireNonNull(ognlBeanInfoCacheFactory).buildOgnlCache(); + this.ognlGuard = requireNonNull(ognlGuard); } @Inject @@ -624,6 +617,9 @@ public class OgnlUtil { expressionCache.put(expr, tree); } } + if (ognlGuard.isBlocked(expr, tree)) { + throw new OgnlException("Expression blocked by OgnlGuard: " + expr); + } return tree; } diff --git a/core/src/main/java/org/apache/struts2/StrutsConstants.java b/core/src/main/java/org/apache/struts2/StrutsConstants.java index 29dfca4b9..143b8c832 100644 --- a/core/src/main/java/org/apache/struts2/StrutsConstants.java +++ b/core/src/main/java/org/apache/struts2/StrutsConstants.java @@ -364,6 +364,9 @@ public final class StrutsConstants { /** The maximum length of an expression (OGNL) */ public static final String STRUTS_OGNL_EXPRESSION_MAX_LENGTH = "struts.ognl.expressionMaxLength"; + /** Parsed OGNL expressions which contain these node types will be blocked */ + public static final String STRUTS_OGNL_EXCLUDED_NODE_TYPES = "struts.ognl.excludedNodeTypes"; + /** Disables {@link org.apache.struts2.dispatcher.StrutsRequestWrapper} request attribute value stack lookup (JSTL accessibility) */ public static final String STRUTS_DISABLE_REQUEST_ATTRIBUTE_VALUE_STACK_LOOKUP = "struts.disableRequestAttributeValueStackLookup"; diff --git a/core/src/main/resources/struts-beans.xml b/core/src/main/resources/struts-beans.xml index fc1fb2ee7..89acda7c6 100644 --- a/core/src/main/resources/struts-beans.xml +++ b/core/src/main/resources/struts-beans.xml @@ -166,6 +166,7 @@ class="com.opensymphony.xwork2.validator.DefaultValidatorFileParser"/> <bean class="com.opensymphony.xwork2.ognl.OgnlUtil"/> + <bean type="com.opensymphony.xwork2.ognl.OgnlGuard" class="com.opensymphony.xwork2.ognl.DefaultOgnlGuard"/> <bean type="com.opensymphony.xwork2.util.TextParser" name="struts" class="com.opensymphony.xwork2.util.OgnlTextParser" scope="singleton"/>