GitHub Advisoryadvisory · NuGet
GHSA-m2p3-hwv5-xpqw Scriban: Denial of Service via Unbounded Cumulative Template Output Bypassing LimitToString
MEDIUM
Official page ↗
- Ecosystem
- NuGet
- Packages
- Scriban, Scriban.Signed
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
- Weaknesses
- CWE-770
- Reviewed
- 2026-03-24
Affected packages
• NuGet Scriban: < 7.0.0 → fixed in 7.0.0
• NuGet Scriban.Signed: < 7.0.0 → fixed in 7.0.0
Description
Summary
The `LimitToString` safety limit (default 1MB since commit `b5ac4bf`) can be bypassed to allocate approximately 1GB of memory by exploiting the per-call reset of `_currentToStringLength` in `ObjectToString`. Each template expression rendered through `TemplateContext.Write(SourceSpan, object)` triggers a separate top-level `ObjectToString` call that resets the length counter to zero, and the underlying `StringBuilderOutput` has no cumulative output size limit. An attacker who can supply a template can cause an out-of-memory condition in the host application.
Details
The root cause is in `TemplateContext.Helpers.cs`, in the `ObjectToString` method:
```csharp
// src/Scriban/TemplateContext.Helpers.cs:89-111
public virtual string ObjectToString(object value, bool nested = false)
{
if (_objectToStringLevel == 0)
{
_currentToStringLength = 0; // 0 && _objectToStringLevel == 1 && result != null && result.Length >= LimitToString)
{
return result + "...";
}
return result;
}
// ...
}
```
Each time a template expression is rendered, `TemplateContext.Write(SourceSpan, object)` calls `ObjectToString`:
```csharp
// src/Scriban/TemplateContext.cs:693-701
public virtual TemplateContext Write(SourceSpan span, object textAsObject)
{
if (textAsObject != null)
{
var text = ObjectToString(textAsObject); // fresh _currentToStringLength = 0
Write(text);
}
return this;
}
```
The `StringBuilderOutput.Write` method appends unconditionally with no size check:
```csharp
// src/Scriban/Runtime/StringBuilderOutput.cs:47-50
public void Write(string text, int offset, int count)
{
Builder.Append(text, offset, count); // no cumulative limit
}
```
Execution flow:
1. Template creates a string of length 1,048,575 (one byte under the 1MB `LimitToString` default)
2. A `for` loop iterates up to `LoopLimit` (default 1000) times
3. Each iteration renders the string via `Write(span, x)` → `ObjectToString(x)`
4. `ObjectToString` resets `_currentToStringLength = 0` since `_objectToStringLevel == 0`
5. The string passes the `LimitToString` check (1,048,575
/// Gets or sets the maximum total output length in characters. Default is 10485760 (10 MB). 0 means no limit.
///
public int OutputLimit { get; set; } = 10485760;
// In TemplateContext.Write(string, int, int) — add check before writing
public TemplateContext Write(string text, int startIndex, int count)
{
if (text != null)
{
if (OutputLimit > 0)
{
_totalOutputLength += count;
if (_totalOutputLength > OutputLimit)
{
throw new ScriptRuntimeException(CurrentSpan,
$"The output limit of {OutputLimit} characters was reached.");
}
}
// ... existing indent/write logic
}
return this;
}
```
This provides defense-in-depth: `LimitToString` caps individual object serialization, while `OutputLimit` caps total template output.
Related
Weakness · CWE (1)
- CWE-770 Allocation of Resources Without Limits or Throttling weakness