PT-2026-30297 · Nuget · Scriban
Publicado
2026-03-24
·
Atualizado
2026-03-24
CVSS v3.1
7.5
Alta
| Vetor | AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H |
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()atTemplateContext.Helpers.cs:98checksObjectRecursionLimit(default 20)EnterRecursive()atTemplateContext.cs:957callsRuntimeHelpers.EnsureSufficientExecutionStack()CheckAbort()atTemplateContext.cs:464also callsEnsureSufficientExecutionStack()
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 inScriptObject.Storedictionary - Pipes to
object.to json:x | object.to json→ callsToJson()at line 477 ToJson()callsWriteValue(context, writer, value)at line 488WriteValueenters theelsebranch (line 515), gets members via accessor, finds "self"TryGetValuereturnsxitself,WriteValuerecurses with the same object — infinite loopStackOverflowExceptionis thrown — fatal, cannot be caught, process terminates
PoC
scriban
{{ x = {}; x.self = x; x | object.to json }}In a hosting application:
csharp
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 hereEven without circular references, deeply nested objects can exhaust the stack since no depth limit is enforced:
scriban
{{ 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 —
StackOverflowExceptionterminates the .NET process. - No try/catch protection possible: Unlike most exceptions,
StackOverflowExceptioncannot be caught by application code. The hosting application cannot wraptemplate.Render()in a try/catch to survive this. - No authentication required:
object.to jsonis a default builtin function (registered inBuiltinFunctions.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:csharp
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");
}
try
{
RuntimeHelpers.EnsureSufficientExecutionStack();
}
catch (InsufficientExecutionStackException)
{
throw new ScriptRuntimeException(context.CurrentSpan,
"Exceeding recursive depth limit in object.to json, near to stack overflow");
}
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, depth + 1);
}
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, depth + 1);
}
}
writer.WriteEndObject();
}
}Correção
Uncontrolled Recursion
Encontrou algum problema na descrição? Tem algo a acrescentar? Fique à vontade para nos escrever 👾
Enumeração de Fraquezas
Identificadores relacionados
Produtos afetados
Scriban