Skip to content

LoopLimit enforcement fails when using nested loops #615

@geoffreymcgill

Description

@geoffreymcgill

Scriban's loop limit enforcement fails when using nested loops, allowing templates to exceed the configured LoopLimit without throwing the expected TemplateEvaluationException.

Expected Behavior

When a template contains loops that exceed the configured LoopLimit, Scriban should throw a TemplateEvaluationException with the message:

Exceeding number of iteration limit `{limit}` for loop statement.

Actual Behavior

Templates with nested loops that exceed the LoopLimit render successfully without throwing any exception, bypassing the loop limit enforcement entirely.

Minimal Reproduction

using System;
using System.Linq;
using Scriban;

class Program
{
    static void Main()
    {
        // BUG: Nested loops bypass loop limit enforcement
        var template = Template.Parse(@"
{{~
items = [" + string.Join(",\n", Enumerable.Range(1, 300).Select(i =>
        $"  {{ name: 'item{i}', tags: ['tag{i}'] }}")) + @"
]
~}}
{{~ for item in items ~}}
{{ item.name }} {{~ for tag in item.tags ~}}{{ tag }}{{~ end ~}}
{{~ end ~}}
");

        var context = new TemplateContext { LoopLimit = 200 };

        try
        {
            var result = template.Render(context);
            Console.WriteLine($"BUG: Should have thrown TemplateEvaluationException");
            Console.WriteLine($"Loop limit: {context.LoopLimit}");
            Console.WriteLine($"Items processed: 300"); // Exceeds limit of 200
        }

        catch (Exception ex)
        {
            Console.WriteLine($"Working correctly: {ex.Message}");
        }
    }
}

Test Results

  • Single loop with 300 items: ✅ Correctly throws exception (loop limit works)
  • Nested loops with 300 items: ❌ Bug - succeeds when it should fail (loop limit bypassed)

Root Cause Analysis

The issue appears to be in the TemplateContext.StepLoop method, which is responsible for tracking loop iterations and enforcing the LoopLimit. The method fails to properly count iterations when there are nested loop structures.

Problematic Pattern:

{{~ for item in items ~}}          <!-- Outer loop: 300 iterations -->
  {{~ for tag in item.tags ~}}     <!-- Inner loop: 1 iteration per item -->
    {{ tag }}
  {{~ end ~}}
{{~ end ~}}

Impact

This bug allows templates to consume excessive resources as the loop limit safety mechanism is bypassed.

Workaround

Avoid using nested loops in templates where loop limit enforcement is critical, or implement additional validation in the application layer to detect and prevent excessive loop iterations.

I will try and figure out the fix required within Scriban.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions