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