GitHub Security Advisory: GHSA-xcx6-vp38-8hr5
Package: Scriban (NUGET)
Severity: HIGH (CVSS 7.5)
CWE: CWE-674: Uncontrolled Recursion
Vulnerable versions: < 7.0.0
Patched in: 7.0.0

Summary: Scriban has Uncontrolled Recursion in `object.to_json` Causing Unrecoverable Process Crash via StackOverflowException

Details:
## Summary

The `object.to_json` builtin function in Scriban performs recursive JSON serialization via an internal `WriteValue()` static local function that has no depth limit, no circular reference detection, and no stack overflow guard. A Scriban template containing a self-referencing object passed to `object.to_json` triggers unbounded recursion, causing a `StackOverflowException` that terminates the hosting .NET process. This is a fatal, unrecoverable crash — `StackOverflowException` cannot be caught by user code in .NET.

## Details

The vulnerable code is the `WriteValue()` static local function at `src/Scriban/Functions/ObjectFunctions.cs:494`:

```csharp
static void WriteValue(TemplateContext context, Utf8JsonWriter writer, object value)
{
    var type = value?.GetType() ?? typeof(object);
    if (value is null || value is string || value is bool ||
        type.IsPrimitiveOrDecimal() || value is IFormattable)
    {
        JsonSerializer.Serialize(writer, value, type);
    }
    else if (value is IList || type.IsArray) {
        writer.WriteStartArray();
        foreach (var x in context.ToList(context.CurrentSpan, value))
        {
            WriteValue(context, writer, x);  // recursive, no depth check
        }
        writer.WriteEndArray();
    }
    else {
        writer.WriteStartObject();
        var accessor = context.GetMemberAccessor(value);
        foreach (var member in accessor.GetMembers(context, context.CurrentSpan, value))
        {
            if (accessor.TryGetValue(context, context.CurrentSpan, value, member, out var memberValue))
            {
                writer.WritePropertyName(member);
                WriteValue(context, writer, memberValue);  // recursive, no depth check
            }
        }
        writer.WriteEndObject();
    }
}
```

This function has **none** of the safety mechanisms present in other recursive paths:

- `ObjectToString()` at `TemplateContext.Helpers.cs:98` checks `ObjectRecursionLimit` (default 20)
- `EnterRecursive()` at `TemplateContext.cs:957` calls `RuntimeHelpers.EnsureSufficientExecutionStack()`
- `CheckAbort()` at `TemplateContext.cs:464` also calls `EnsureSufficientExecutionStack()`

The `WriteValue()` function bypasses all of these because it is a static local function that only takes the `TemplateContext` for member access — it never calls `EnterRecursive()`, never checks `ObjectRecursionLimit`, and never calls `EnsureSufficientExecutionStack()`.

**Execution flow:**

1. Template creates a ScriptObject: `{{ x = {} }}`
2. Sets a self-reference: `x.self = x` — stores a reference in `ScriptObject.Store` dictionary
3. Pipes to `object.to_json`: `x | object.to_json` → calls `ToJson()` at line 477
4. `ToJson()` calls `WriteValue(context, writer, value)` at line 488
5. `WriteValue` enters the `else` branch (line 515), gets members via accessor, finds "self"
6. `TryGetValue` returns `x` itself, `WriteValue` recurses with the same object — infinite loop
7. `StackOverflowException` i