Improper resource allocation In scriban
Description
Scriban has Uncontrolled Recursion in object.to_json Causing Unrecoverable Process Crash via StackOverflowException
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:
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); }...
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:
Template creates a ScriptObject: {{ x = {} }}
Sets a self-reference: x.self = x — stores a reference in ScriptObject.Store dictionary
Pipes to object.to_json: x | object.to_json → calls ToJson() at line 477
ToJson() calls WriteValue(context, writer, value) at line 488
WriteValue enters the else branch (line 515), gets members via accessor, finds "self"
TryGetValue returns x itself, WriteValue recurses with the same object — infinite loop
StackOverflowException is thrown — fatal, cannot be caught, process terminates
PoC
{{ x = {}; x.self = x; x | object.to_json }}
In a hosting application:
using Scriban; // This will crash the entire process with StackOverflowException var template = Template.Parse("{{ x = {}; x.self = x; x | object.to_json }}"); var result = template.Render(); // FATAL: process terminates here
Even without circular references, deeply nested objects can exhaust the stack since no depth limit is enforced:
{{ a = {} b = {inner: a} c = {inner: b} d = {inner: c} # ... continue nesting ... result = deepest | object.to_json }}
Impact
Process crash DoS: Any application embedding Scriban for user-provided templates (CMS platforms, email template engines, report generators, static site generators) can be crashed by a single malicious template. The crash is unrecoverable — StackOverflowException terminates the .NET process.
No try/catch protection possible: Unlike most exceptions, StackOverflowException cannot be caught by application code. The hosting application cannot wrap template.Render() in a try/catch to survive this.
No authentication required: object.to_json is a default builtin function (registered in BuiltinFunctions.cs), available in all Scriban templates unless explicitly removed.
Trivial to exploit: The PoC is a single line of template code.
Recommended Fix
Add a depth counter parameter to WriteValue() and check it against ObjectRecursionLimit, consistent with how ObjectToString is protected. Also add EnsureSufficientExecutionStack() as a safety net:
static void WriteValue(TemplateContext context, Utf8JsonWriter writer, object value, int depth = 0) { if (context.ObjectRecursionLimit != 0 && depth > context.ObjectRecursionLimit) { throw new ScriptRuntimeException(context.CurrentSpan, $"Exceeding object recursion limit `{context.ObjectRecursionLimit}` in object.to_json"); } ...
Mitigation
Update Impact
Minimal update. May introduce new vulnerabilities or breaking changes.
Ecosystem | Package | Affected version | Patched versions |
|---|---|---|---|
nuget | scriban | 7.0.0 |
Aliases