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 7106acf599ef426a3b8230f527ea18831f775af7 Author: Kusal Kithul-Godage <g...@kusal.io> AuthorDate: Thu Aug 31 19:21:51 2023 +1000 WW-5340 Introducing OGNL Guard --- .../opensymphony/xwork2/ognl/DefaultOgnlGuard.java | 79 ++++++++++++++++++++++ .../com/opensymphony/xwork2/ognl/OgnlGuard.java | 37 ++++++++++ .../com/opensymphony/xwork2/ognl/OgnlUtil.java | 9 +++ .../java/org/apache/struts2/StrutsConstants.java | 3 + core/src/main/resources/struts-beans.xml | 1 + 5 files changed, 129 insertions(+) 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..b9ae31455 100644 --- a/core/src/main/java/com/opensymphony/xwork2/ognl/OgnlUtil.java +++ b/core/src/main/java/com/opensymphony/xwork2/ognl/OgnlUtil.java @@ -73,6 +73,7 @@ public class OgnlUtil { private final OgnlCache<String, Object> expressionCache; private final OgnlCache<Class<?>, BeanInfo> beanInfoCache; private TypeConverter defaultConverter; + private OgnlGuard ognlGuard; private boolean devMode; private boolean enableExpressionCache = true; @@ -140,6 +141,11 @@ public class OgnlUtil { this.beanInfoCache = ognlBeanInfoCacheFactory.buildOgnlCache(); } + @Inject + protected void setOgnlGuard(OgnlGuard ognlGuard) { + this.ognlGuard = ognlGuard; + } + @Inject protected void setXWorkConverter(XWorkConverter conv) { this.defaultConverter = new OgnlTypeConverterWrapper(conv); @@ -624,6 +630,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"/>