From 51124ff11484c91f91dc48cc8095582b96fc36a9 Mon Sep 17 00:00:00 2001 From: ThisaraWeerakoon Date: Fri, 6 Nov 2026 09:33:02 +0530 Subject: [PATCH] Disallow document() and evaluate() in re-compiled template parameters A dynamic template parameter in the escaped {{...}} form is evaluated once and its result is re-compiled into a new XPath expression. Since that expression is derived from a runtime value, it is now rejected if it calls document() or evaluate(). All other functions and the single-evaluation parameter forms are unchanged. --- .../org/apache/synapse/mediators/Value.java | 96 ++++++++++++++++- .../apache/synapse/mediators/ValueTest.java | 101 ++++++++++++++++++ 2 files changed, 194 insertions(+), 3 deletions(-) create mode 100644 modules/core/src/test/java/org/apache/synapse/mediators/ValueTest.java diff --git a/modules/core/src/main/java/org/apache/synapse/mediators/Value.java b/modules/core/src/main/java/org/apache/synapse/mediators/Value.java index 3bedd5d50..7622954ea 100644 --- a/modules/core/src/main/java/org/apache/synapse/mediators/Value.java +++ b/modules/core/src/main/java/org/apache/synapse/mediators/Value.java @@ -26,10 +26,23 @@ import org.apache.synapse.SynapseException; import org.apache.synapse.util.xpath.SynapseXPath; import org.jaxen.JaxenException; +import org.jaxen.expr.BinaryExpr; +import org.jaxen.expr.Expr; +import org.jaxen.expr.FilterExpr; +import org.jaxen.expr.FunctionCallExpr; +import org.jaxen.expr.LocationPath; +import org.jaxen.expr.PathExpr; +import org.jaxen.expr.Predicate; +import org.jaxen.expr.Step; +import org.jaxen.expr.UnaryExpr; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Set; /** * Represents a Value @@ -40,6 +53,16 @@ public class Value { private static final Log log = LogFactory.getLog(Value.class); + /** + * Functions that a re-compiled dynamic ("{{...}}") template parameter expression is not + * allowed to call. document() resolves an external resource and evaluate() compiles and + * runs a further expression at runtime; neither is evaluated for an expression that was + * itself produced from a runtime-derived value. Such a call is reported as an error. + */ + private static final Set RESTRICTED_DYNAMIC_FUNCTIONS = + Collections.unmodifiableSet( + new HashSet(Arrays.asList("document", "evaluate"))); + /** * Name of the value attribute */ @@ -195,6 +218,7 @@ public boolean hasPropertyEvaluateExpr(){ public Object evalutePropertyExpression(MessageContext synCtx) { SynapseXPath expr = null; + SynapseXPath expression; try { expr = new SynapseXPath(this.keyValue.substring(1, keyValue.length() - 1)); for (OMNamespace aNamespaceList : namespaceList) { @@ -202,16 +226,82 @@ public Object evalutePropertyExpression(MessageContext synCtx) { } String result = expr.stringValueOf(synCtx); - SynapseXPath expression = new SynapseXPath(result); + expression = new SynapseXPath(result); for (OMNamespace aNamespaceList : namespaceList) { expression.addNamespace(aNamespaceList); } - return expression; } catch (Exception e) { handleException("Can not evaluate escaped expression : " + expr); + return this.expression; } - return this.expression; + String restrictedFunction = findRestrictedFunction(expression.getRootExpr()); + if (restrictedFunction != null) { + handleException("The function '" + restrictedFunction + "' is not allowed in a " + + "dynamically evaluated template parameter expression"); + } + return expression; + } + + /** + * Walks a parsed XPath expression tree and returns the name of the first function call + * that is in {@link #RESTRICTED_DYNAMIC_FUNCTIONS}, or {@code null} if there is none. The + * whole tree is inspected, including function arguments and step predicates. + * + * @param node a node of the parsed expression tree (an {@link Expr}, {@link Step} or + * {@link Predicate}); may be {@code null} + * @return the restricted function name if one is present, otherwise {@code null} + */ + private String findRestrictedFunction(Object node) { + if (node == null) { + return null; + } + if (node instanceof FunctionCallExpr) { + FunctionCallExpr call = (FunctionCallExpr) node; + String prefix = call.getPrefix(); + String functionName = call.getFunctionName(); + if ((prefix == null || prefix.isEmpty()) && functionName != null + && RESTRICTED_DYNAMIC_FUNCTIONS.contains(functionName)) { + return functionName; + } + return findRestrictedFunction(call.getParameters()); + } + if (node instanceof PathExpr) { + PathExpr path = (PathExpr) node; + String found = findRestrictedFunction(path.getFilterExpr()); + return found != null ? found : findRestrictedFunction(path.getLocationPath()); + } + if (node instanceof FilterExpr) { + FilterExpr filter = (FilterExpr) node; + String found = findRestrictedFunction(filter.getExpr()); + return found != null ? found : findRestrictedFunction(filter.getPredicates()); + } + if (node instanceof BinaryExpr) { + BinaryExpr binary = (BinaryExpr) node; + String found = findRestrictedFunction(binary.getLHS()); + return found != null ? found : findRestrictedFunction(binary.getRHS()); + } + if (node instanceof UnaryExpr) { + return findRestrictedFunction(((UnaryExpr) node).getExpr()); + } + if (node instanceof LocationPath) { + return findRestrictedFunction(((LocationPath) node).getSteps()); + } + if (node instanceof Step) { + return findRestrictedFunction(((Step) node).getPredicates()); + } + if (node instanceof Predicate) { + return findRestrictedFunction(((Predicate) node).getExpr()); + } + if (node instanceof List) { + for (Object child : (List) node) { + String found = findRestrictedFunction(child); + if (found != null) { + return found; + } + } + } + return null; } public void setNamespaces(OMElement elem){ diff --git a/modules/core/src/test/java/org/apache/synapse/mediators/ValueTest.java b/modules/core/src/test/java/org/apache/synapse/mediators/ValueTest.java new file mode 100644 index 000000000..64f9f4392 --- /dev/null +++ b/modules/core/src/test/java/org/apache/synapse/mediators/ValueTest.java @@ -0,0 +1,101 @@ +/* + * 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 org.apache.synapse.mediators; + +import junit.framework.TestCase; +import org.apache.synapse.MessageContext; +import org.apache.synapse.SynapseException; +import org.apache.synapse.util.xpath.SynapseXPath; + +/** + * Tests for {@link Value#evalutePropertyExpression(MessageContext)}, which evaluates a dynamic + * ("{{...}}") template parameter once and re-compiles the resulting string as a new XPath + * expression. The re-compiled expression keeps working for ordinary functions, but a + * {@code document()} or {@code evaluate()} call in it is reported as an error instead of being + * turned into an evaluatable expression. + */ +public class ValueTest extends TestCase { + + /** + * Builds an escaped-expression {@link Value} whose first evaluation returns exactly + * {@code recompiledExpression}. The inner text of the {{...}} form is wrapped as an XPath + * string literal, so the result does not depend on any property or message content. + * + * @param recompiledExpression the XPath text the first evaluation should yield + * @return a Value that re-compiles {@code recompiledExpression} on evaluation + */ + private Value escapedValueYielding(String recompiledExpression) { + return new Value("{'" + recompiledExpression + "'}"); + } + + private void assertRejected(String recompiledExpression) throws Exception { + MessageContext synCtx = TestUtils.getTestContext(""); + try { + escapedValueYielding(recompiledExpression).evalutePropertyExpression(synCtx); + fail("expected a SynapseException for re-compiled expression: " + recompiledExpression); + } catch (SynapseException expected) { + // the re-compiled expression calls a restricted function and must not be evaluated + } + } + + /** A re-compiled expression without a restricted function must still be returned. */ + public void testBenignExpressionIsStillRecompiled() throws Exception { + MessageContext synCtx = TestUtils.getTestContext(""); + Object result = + escapedValueYielding("concat(\"F010\",\"-\",\"PWN\")").evalutePropertyExpression(synCtx); + assertTrue("a re-compiled expression without a restricted function must be returned as " + + "a SynapseXPath", result instanceof SynapseXPath); + } + + public void testDocumentFunctionIsRejected() throws Exception { + assertRejected("document(\"http://example.com/f010\")"); + } + + public void testDocumentFunctionTwoArgFormIsRejected() throws Exception { + assertRejected("document(\"http://example.com/f010\", /)"); + } + + public void testEvaluateFunctionIsRejected() throws Exception { + assertRejected("evaluate(\"count(//a)\")"); + } + + public void testDocumentNestedInFunctionArgumentIsRejected() throws Exception { + assertRejected("concat(document(\"http://example.com/f010\"), \"\")"); + } + + public void testDocumentWrappedInFunctionIsRejected() throws Exception { + assertRejected("string(document(\"http://example.com/f010\"))"); + } + + public void testDocumentInStepPredicateIsRejected() throws Exception { + assertRejected("//a[document(\"http://example.com/f010\")]"); + } + + /** + * Only the unprefixed document()/evaluate() are restricted; a namespaced function that + * happens to share the local name is a different function and is left alone. + */ + public void testNamespacedDocumentIsNotRestricted() throws Exception { + MessageContext synCtx = TestUtils.getTestContext(""); + Object result = escapedValueYielding("ns:document(\"x\")").evalutePropertyExpression(synCtx); + assertTrue("a namespaced function sharing the local name must not be restricted", + result instanceof SynapseXPath); + } +}