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);
}
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
{{ 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 —
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:
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();
}
}
Software afectado
Haz clic en una tecnología para ver todas sus vulnerabilidades.
| Plataforma | Paquete | Versiones vulnerables | Corregida en |
|---|---|---|---|
| .NET (NuGet) | Scriban | < 7.0.0 | 7.0.0 |
| .NET (NuGet) | Scriban.Signed | < 7.0.0 | 7.0.0 |
Referencias
- Vector CVSS
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H- Debilidad (CWE)
CWE-674- Plataforma
- .NET (NuGet)
- Paquete
Scriban, Scriban.Signed- Publicada
- 24.03.2026
- Actualizado
- 11.10.2026 11:23
Fuentes y licencias
OSV / GitHub Advisory Database - GHSA-xcx6-vp38-8hr5 - CC BY 4.0
Historial de vulnerabilidades: Scriban
Todas las vulnerabilidades publicadas de este paquete (.NET (NuGet)), de la más reciente a la más antigua. Muestra si es un fallo aislado o un paquete que hay que vigilar.
- Entradas en total
- 15
- Primera → última
- 03.2026 07.2026
- Sin corrección
- 0
- Explotada en ataques
- 0
Distribución por gravedad
- Crítica 1
- Alta 9
- Media 5
Divulgaciones por año
- 2026: 15
-
Scriban has Uncontrolled Recursion in `object.to_json` Causing Unrecoverable Process Crash via StackOverflowException esta entrada
Identifica las vulnerabilidades de tu aplicación
Con la integración de DockRay y el inventario de software activado, DockRay compara las versiones notificadas de los componentes con las vulnerabilidades conocidas y avisa de las coincidencias confirmadas. La cobertura depende de la tecnología compatible y de los avisos disponibles.