You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@velocity.apache.org by cb...@apache.org on 2026/09/15 12:11:32 UTC
(velocity-engine) 06/09: Fix: #evaluate() max depth should honor directive.parse.max_depth
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/velocity-engine.git
commit ae068a34891b22da1acf5d79cc82992837d3a7bb
Author: Claude Brisson <cl...@renegat.net>
AuthorDate: Tue Sep 15 11:19:04 2026 +0200
Fix: #evaluate() max depth should honor directive.parse.max_depth
---
src/changes/changes.xml | 5 ++
.../apache/velocity/runtime/RuntimeConstants.java | 6 +-
.../velocity/runtime/directive/Evaluate.java | 32 ++++++++++
.../velocity/runtime/defaults/velocity.properties | 4 ++
.../org/apache/velocity/test/EvaluateTestCase.java | 69 ++++++++++++++++++++++
5 files changed, 115 insertions(+), 1 deletion(-)
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index e47a4db5..59148f9e 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -39,6 +39,11 @@
<body>
<release version="2.5" date="2026-06-14">
+ <action type="fix" dev="cbrisson">
+ <code>#evaluate</code> now honours the nesting depth limit set by
+ <code>directive.parse.max_depth</code> (10 by default, zero or less meaning no
+ limit).
+ </action>
<action type="fix" dev="cbrisson" due-to="ogawaffm" issue="VELOCITY-996">
Fix precision loss when converting <code>Long</code> values to
<code>BigDecimal</code> in VTL math operations.
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeConstants.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeConstants.java
index 8cedd936..de25cf41 100644
--- a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeConstants.java
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/RuntimeConstants.java
@@ -105,7 +105,11 @@ public interface RuntimeConstants extends DeprecatedRuntimeConstants
*/
String ERRORMSG_END = "directive.include.output_error_end";
- /** Maximum recursion depth allowed for the #parse directive. */
+ /**
+ * Maximum nesting depth allowed for the #parse and #evaluate directives, which share the
+ * same template name stack: a chain mixing both directives is counted as a whole
+ * (#evaluate honours this limit since 2.5).
+ */
String PARSE_DIRECTIVE_MAXDEPTH = "directive.parse.max_depth";
/** Maximum recursion depth allowed for the #define directive. */
diff --git a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Evaluate.java b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Evaluate.java
index e6c2aa28..b13db11a 100644
--- a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Evaluate.java
+++ b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/directive/Evaluate.java
@@ -25,6 +25,7 @@
import org.apache.velocity.exception.ParseErrorException;
import org.apache.velocity.exception.ResourceNotFoundException;
import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.RuntimeServices;
import org.apache.velocity.runtime.parser.ParseException;
import org.apache.velocity.runtime.parser.node.Node;
@@ -39,6 +40,11 @@
/**
* Evaluates the directive argument as a VTL string, using the existing
* context.
+ * <p>
+ * The nesting depth is limited by the <code>directive.parse.max_depth</code> property
+ * (10 by default, zero or less meaning unlimited): <code>#evaluate</code> and
+ * <code>#parse</code> share the same template name stack, so a chain mixing both
+ * directives is counted as a whole against that one limit.
*
* @author <a href="mailto:wglass@apache.org">Will Glass-Husain</a>
* @version $Id$
@@ -46,6 +52,10 @@
*/
public class Evaluate extends Directive
{
+ /**
+ * Maximum nesting depth, shared with #parse, zero or less meaning unlimited.
+ */
+ private int maxDepth;
/**
* Return name of this directive.
@@ -81,6 +91,8 @@ public void init(RuntimeServices rs, InternalContextAdapter context,
{
super.init( rs, context, node );
+ this.maxDepth = rsvc.getInt(RuntimeConstants.PARSE_DIRECTIVE_MAXDEPTH, 10);
+
/*
* Check that there is exactly one argument and it is a string or reference.
*/
@@ -143,6 +155,26 @@ public boolean render(InternalContextAdapter context, Writer writer,
ParseErrorException, MethodInvocationException
{
+ if (maxDepth > 0)
+ {
+ /*
+ * see if we have exceeded the configured max depth.
+ */
+ String[] templateStack = context.getTemplateNameStack();
+ if (templateStack.length >= maxDepth)
+ {
+ StringBuilder path = new StringBuilder();
+ for (String aTemplateStack : templateStack)
+ {
+ path.append(" > ").append(aTemplateStack);
+ }
+ log.error("#{}(): max recursion depth reached ({}). File stack: {}",
+ getName(), maxDepth, path);
+
+ return false;
+ }
+ }
+
/*
* Evaluate the string with the current context. We know there is
* exactly one argument and it is a string or reference.
diff --git a/velocity-engine-core/src/main/resources/org/apache/velocity/runtime/defaults/velocity.properties b/velocity-engine-core/src/main/resources/org/apache/velocity/runtime/defaults/velocity.properties
index 38cdb51e..a00a05db 100644
--- a/velocity-engine-core/src/main/resources/org/apache/velocity/runtime/defaults/velocity.properties
+++ b/velocity-engine-core/src/main/resources/org/apache/velocity/runtime/defaults/velocity.properties
@@ -49,6 +49,10 @@ directive.if.empty_check = true
# ----------------------------------------------------------------------------
# P A R S E P R O P E R T I E S
# ----------------------------------------------------------------------------
+# Maximum nesting depth of #parse and #evaluate, which share the same template
+# name stack: a chain mixing both directives is counted as a whole. Zero or
+# less means no limit.
+# ----------------------------------------------------------------------------
directive.parse.max_depth = 10
diff --git a/velocity-engine-core/src/test/java/org/apache/velocity/test/EvaluateTestCase.java b/velocity-engine-core/src/test/java/org/apache/velocity/test/EvaluateTestCase.java
index fc5c1d1b..90fa6ef8 100644
--- a/velocity-engine-core/src/test/java/org/apache/velocity/test/EvaluateTestCase.java
+++ b/velocity-engine-core/src/test/java/org/apache/velocity/test/EvaluateTestCase.java
@@ -170,6 +170,75 @@ public void testEventHandler()
}
+ /**
+ * Test that a self-referential #evaluate stops at the default depth
+ * instead of recursing until the stack is exhausted.
+ */
+ public void testEvaluateMaxDepthDefault()
+ {
+ context.put("bomb", "X#evaluate($bomb)");
+
+ /*
+ * the limit is 10 by default and the template itself occupies the first
+ * slot of the shared template name stack, hence nine nested evaluations
+ */
+ assertEvalEquals("XXXXXXXXX", "#evaluate($bomb)");
+ }
+
+ /**
+ * Test that the depth limit is configurable and that reaching it is logged.
+ */
+ public void testEvaluateMaxDepthConfigured()
+ {
+ engine.setProperty(RuntimeConstants.PARSE_DIRECTIVE_MAXDEPTH, "3");
+ context.put("bomb", "X#evaluate($bomb)");
+ log.startCapture();
+
+ assertEvalEquals("XX", "#evaluate($bomb)");
+
+ assertTrue("expected the limit to be logged, got: " + log.getLog(),
+ log.getLog().contains("#evaluate(): max recursion depth reached (3)"));
+ }
+
+ /**
+ * Test that a zero or negative limit means no limit.
+ */
+ public void testEvaluateMaxDepthUnlimited()
+ {
+ engine.setProperty(RuntimeConstants.PARSE_DIRECTIVE_MAXDEPTH, "0");
+ context.put("i", 0);
+ // recursion that stops by itself, deeper than the default limit would allow
+ context.put("rec", "X#set($i = $i + 1)#if($i < 15)#evaluate($rec)#end");
+
+ assertEvalEquals("XXXXXXXXXXXXXXX", "#evaluate($rec)");
+ }
+
+ /**
+ * Test that the limit does not affect an #evaluate which does not recurse.
+ */
+ public void testEvaluateMaxDepthNoRecursion()
+ {
+ // 2 is the lowest limit leaving room for one evaluation, the template being the first
+ engine.setProperty(RuntimeConstants.PARSE_DIRECTIVE_MAXDEPTH, "2");
+ context.put("foo", "bar");
+
+ assertEvalEquals("bar", "#evaluate('$foo')");
+ }
+
+ /**
+ * Test that #parse and #evaluate share the limit, since they share the
+ * template name stack.
+ */
+ public void testEvaluateMaxDepthSharedWithParse()
+ {
+ engine.setProperty(RuntimeConstants.PARSE_DIRECTIVE_MAXDEPTH, "3");
+ addTemplate("shared.vm", "P#evaluate($bomb)");
+ context.put("bomb", "X#evaluate($bomb)");
+
+ // the #parse takes the second slot, so a single nested evaluation is left
+ assertEvalEquals("PX", "#parse('shared.vm')");
+ }
+
/**
* Test errors are thrown
* @throws Exception