<!-- obsidian --><h2 data-heading="Description">Description</h2>
<p>When a host pushes a CLR object into a Scriban <code>TemplateContext</code> via the standard, documented pattern —</p>
<pre><code class="language-csharp">var so = new ScriptObject();
so["user"] = currentUser; // direct CLR reference
context.PushGlobal(so);
</code></pre>
<p>— <code>TypedObjectAccessor</code> exposes every public-getter property for <strong>both reading and writing</strong>, and writes land on the live host object and <strong>persist after <code>Render()</code> returns</strong>. The write path performs no <code>CanWrite</code> and no setter-visibility check, producing two related but distinct weaknesses:</p>
<p><strong>(A) Mass assignment of public setters — CWE-915 (originally F-002).</strong> Any <code>{ get; set; }</code> property is writable from template code (<code>{{ user.is_admin = true }}</code>, <code>{{ order.total_price = 0 }}</code>). This is "surprising but technically consistent with the setter being public" — and crucially, Scriban offers <strong>no way to expose such a property read-only</strong>, because <code>MemberFilter</code> is read/write-symmetric.</p>
<p><strong>(B) Access-modifier bypass — CWE-284 (originally F-007).</strong> Properties the developer <strong>deliberately</strong> restricted are also writable, because reflection ignores C# accessibility:</p>
Declaration | Developer intent | Actual behavior
-- | -- | --
{ get; set; } | writable | writable (mass assignment — A)
{ get; private set; } | only the owning class writes | template writes freely
{ get; internal set; } | only the declaring assembly writes | template writes freely
{ get; init; } | immutable after construction (C# 9 language guarantee) | template writes freely post-construction
<p>The <code>init</code>-only post-construction write — the highest false-positive risk — was explicitly confirmed against the shipped 7.2.1 package.</p>
<h2 data-heading="Affected Versions">Affected Versions</h2>
<p>All releases that ship <code>TypedObjectAccessor</code> (<code><= 7.2.1</code>). <code>PrepareMembers</code> has used the getter-only filter since the accessor was introduced, and <code>TrySetValue</code> has never checked the setter. The <code>init</code> bypass applies on .NET 5+; <code>private set</code> / <code>internal set</code> apply on every supported runtime. No patched version exists.</p>
<h2 data-heading="Steps to Reproduce">Steps to Reproduce</h2>
<blockquote>
<p>Copy-paste. Run from the engagement root (the folder containing both <code>scriban/</code> and <code>reports/</code>).</p>
</blockquote>
<p><strong>Prereqs:</strong></p>
<pre><code class="language-bash">test -d scriban || { echo "scriban source missing"; exit 1; }
( command -v dotnet >/dev/null && dotnet --list-sdks | grep -q '^10\.' ) \
|| ( "$HOME/.dotnet/dotnet" --list-sdks | grep -q '^10\.' ) \
|| { echo ".NET 10 SDK missing"; exit 1; }
export PATH="$HOME/.dotnet:$PATH"
</code></pre>
<p><strong>Run both PoCs (native):</strong></p>
<pre><code class="language-bash">( cd reports/f002/poc && dotnet run -c Release ) # (A) public-setter mass assignment
( cd reports/f007/poc && dotnet run -c Release ) # (B) private/internal/init bypass
</code></pre>
<p><strong>Docker fallback (no native SDK required):</strong></p>
<pre><code class="language-bash">docker run --rm -v "$PWD":/work -w /work/reports/f007/poc \
mcr.microsoft.com/dotnet/sdk:10.0 bash -lc "dotnet run -c Release"
</code></pre>
<p><strong>Confirm the published package is affected (not just master):</strong> swap the <code>ProjectReference</code> in <code>reports/f007/poc/poc.csproj</code> for <code><PackageReference Include="Scriban" Version="7.2.1" /></code> and re-run — the four bypasses still succeed.</p>
<p>Each PoC prints <code>[1]</code> original CLR values, <code>[2]</code> template output (reads originals → writes → reads back), and <code>[3]</code> the <strong>C#-side</strong> read after <code>Render()</code> proving the live host object was permanently altered.</p>
<h2 data-heading="Remediation">Remediation</h2>
<p>Fixes are listed flat. Note that (B) has a clean, clearly-correct code fix; (A) requires a <em>new control</em> because public-setter writes are otherwise by-design.</p>
<ul>
<li><strong>Fix 1 — block restricted setters in <code>TrySetValue</code> (<code>TypedObjectAccessor.cs</code> L108–L123). Fixes (B).</strong> Before the L120 <code>SetValue</code>, require a public, non-<code>init</code> setter:
<pre><code class="language-csharp">var setM = propertyAccessor.GetSetMethod(nonPublic: false);
if (setM is null) return false; // private / internal / protected setters
if (setM.ReturnParameter.GetRequiredCustomModifiers()
.Any(m => m.FullName == "System.Runtime.CompilerServices.IsExternalInit"))
return false; // init-only: setter IS public, so the IsExternalInit check is REQUIRED
</code></pre>
A plain <code>GetSetMethod(
CefSharp.Common's FolderSchemeHandlerFactory contains a path validation flaw that allows serving files outside the configured root folder. The vulnerability arises because the code uses a raw string prefix check to validate paths, which does not enforce directory boundaries. This can allow an attacker to access sibling directories with paths that start with the root folder path string but are not actually contained within it. This issue affects versions prior to 148.0.90. Exploitation requires the attacker to induce the embedded browser to request URLs handled by the vulnerable scheme handler.
### Summary
The `ExpressionDepthLimit` parser guard in Scriban does not actually stop parsing — it only logs a non-fatal error and lets recursive descent continue. As a result, a template containing a deeply nested expression (parentheses, array initializers, object initializers, or unary operators) drives the recursive-descent parser into a native stack overflow. The resulting `StackOverflowException` is **uncatchable** in .NET and **immediately terminates the host process**.
Any application that parses an attacker-influenced template — or that passes attacker-controlled strings to `object.eval` / `object.eval_template` — can be crashed by a single small request (roughly an 8 KB payload). This is a denial-of-service. It affects both Scriban-native (`Template.Parse`) and Liquid (`Template.ParseLiquid`) syntax modes, which share the same expression parser.
This re-opens two advisories that were reported as fixed: **GHSA-wgh7-7m3c-fx25** ("Uncontrolled recursion in parser → StackOverflow", reported fixed in 6.6.0) and **GHSA-p6q4-fgr8-vx4p** ("StackOverflow via nested array initializers bypasses ExpressionDepthLimit", reported fixed in 7.0.0). Both fixes are incomplete: the limit they rely on never halts recursion. **All releases 6.6.0 through 7.2.0 (current) are affected.**
### Details
The depth guard is `EnterExpression()` in `src/Scriban/Parsing/Parser.Expressions.cs`:
```csharp
// src/Scriban/Parsing/Parser.Expressions.cs:1209-1218
private void EnterExpression()
{
_expressionDepth++;
var limit = Options.ExpressionDepthLimit;
if (limit > 0 && !_isExpressionDepthLimitReached && _expressionDepth > limit)
{
LogError(GetSpanForToken(Previous), $"The statement depth limit `{limit}` was reached when parsing this statement");
_isExpressionDepthLimitReached = true;
}
}
```
When the limit is exceeded it calls `LogError(...)` and sets a flag. It does **not** throw, does **not** return a sentinel, and does **not** unwind the parse. `LogError` here uses the default `isFatal: false`, so it merely appends a message and sets `HasErrors` — parsing proceeds:
```csharp
// src/Scriban/Parsing/Parser.cs:476-488
private void Log(LogMessage logMessage, bool isFatal = false)
{
Messages.Add(logMessage);
if (logMessage.Type == ParserMessageType.Error)
{
HasErrors = true;
if (isFatal) _hasFatalError = true; // not set on the depth-limit path
}
}
```
The flag `_isExpressionDepthLimitReached` is consulted **only** to avoid logging the same error more than once — no code path uses it to stop descending. Confirmed by full-repo search (`grep -rn "_isExpressionDepthLimitReached" src/`): it appears in exactly four places — the field declaration (`Parser.cs:40`), a reset to `false` (`Parser.cs:106`), and within `EnterExpression` the dedup test (`Parser.Expressions.cs:1213`) and its assignment to `true` (`:1216`). The only *read* is the dedup test on line 1213; nothing else reads it. `ParseExpression` calls `EnterExpression()` and then continues straight into the token switch with no flag check:
```csharp
// src/Scriban/Parsing/Parser.Expressions.cs:113 + 181-182
EnterExpression();
try
{
...
case TokenType.OpenParen:
leftOperand = ParseParenthesis(); // recurses back into ParseExpression
```
```csharp
// src/Scriban/Parsing/Parser.Expressions.cs:984-1001
private ScriptExpression ParseParenthesis()
{
var expression = Open<ScriptNestedExpression>();
ExpectAndParseTokenTo(expression.OpenParen, TokenType.OpenParen);
expression.Expression = ExpectAndParseExpression(expression); // -> ParseExpression -> ParseParenthesis -> ...
...
}
```
Both `Template.Parse` (Scriban-native) and `Template.ParseLiquid` (Liquid-compatibility) front-ends share this same expression parser, so both entry points are affected.
So for input nested N levels deep, the parser recurses N levels deep regardless of `ExpressionDepthLimit`. There is no `RuntimeHelpers.EnsureSufficientExecutionStack()` call and no absolute recursion cap anywhere in the parser. Once the native thread stack is exhausted, the runtime raises `StackOverflowException`, which .NET does not allow to be caught and which tears down the entire process. The number of nesting levels required to overflow depends on the platform's thread-stack size (empirically around 4,000 levels on a default 1 MB stack); it is not a configurable mitigation.
The same defective guard is what makes the array-initializer fix for GHSA-p6q4-fgr8-vx4p ineffective: `ParseArrayInitializer` was wrapped in `EnterExpression()/LeaveExpression()`, but because `EnterExpression()` only logs, the array path still overflows.
The existing regression tests only assert `HasErrors == true` at a nesting depth of ~20 with a limit of 10 (`src/Scriban.Tests/TestParser.cs`); they never use a depth large enough to overflow the stack, so they pass while the protection does nothing against the actual DoS.
**Runtime reachability without
### Summary
The array multiplication operator (`array * integer`) in Scriban allocates a result whose size is the product of the attacker-controlled integer and the array length, with **no `LoopLimit` / `LimitToString` check and no overflow-safe arithmetic**. A ~40-byte template forces a multi-gigabyte allocation, producing a denial-of-service.
This is the unguarded sibling of operations that *were* hardened against the same class of abuse: `string * integer` (gated by a `LimitToString` pre-check), `array.insert_at` (gated by `StepLoop`/`LoopLimit` — the **GHSA-24c8-4792-22hx** fix shipped in 7.2.0, scored 8.7 High), and the range/iteration paths covered by **GHSA-c875-h985-hvrc** ("Built-in operations bypass LoopLimit", fixed 7.0.0). The same `LoopLimit`-based hardening pattern was applied to those operations but never to `array * integer`.
This can be observed directly in 7.0.0, the release where GHSA-c875 was patched: `(1..5) * 50000000` (and `1..N | array.size`) correctly throws `Exceeding number of iteration limit '1000'`, while `[1,2,3,4,5] * 50000000` allocates ~2 GB with no limit. The `LoopLimit` control is enforced on the iteration path but not on the `array * int` allocation path, side by side, in the same version. The bug has been present since the operator was introduced in **3.0.0**, survives all of the 6.6.0 / 7.0.0 / 7.2.0 DoS-hardening passes, and is still present in 7.2.0 (current) — i.e. it is both a missed sibling of GHSA-24c8 and an incomplete coverage of GHSA-c875's `LoopLimit` hardening.
### Details
The `array * int` operator is handled in `ScriptArray<T>.TryEvaluate`:
```csharp
// src/Scriban/Runtime/ScriptArray.cs:504-508 (Multiply case)
var newArray = new ScriptArray<T>(intModifier * array.Count);
for (int i = 0; i < intModifier; i++)
{
newArray.AddRange(array);
}
```
`intModifier` is the attacker-supplied integer (`context.ToInt(...)`, `ScriptArray.cs:399`). Two problems:
1. **No resource limit.** Neither `new ScriptArray<T>(intModifier * array.Count)` nor the `AddRange` loop consults `LoopLimit`, `LimitToString`, or calls `context.StepLoop(...)`. A grep of the entire `TryEvaluate` method (`ScriptArray.cs:360-560`) finds no `StepLoop` / `LoopLimit` / `Limit` reference. `LoopLimit` (default 1000) is therefore not enforced: a template that requests 250,000,000 elements creates them all without any "iteration limit" error.
2. **Integer overflow in the capacity.** `intModifier * array.Count` is unchecked `int` arithmetic. The overflow-safe `long` cast used by the string sibling is absent here.
The DoS-hardening passes guarded the two sibling operations but not this one:
```csharp
// src/Scriban/Syntax/Expressions/ScriptBinaryExpression.cs:341 (string * int — GUARDED)
if (context.LimitToString > 0 && value > 0 && leftText.Length > 0
&& (long)leftText.Length * value > context.LimitToString) // long arithmetic, pre-check
{
throw new ScriptRuntimeException(spanMultiplier, $"String multiplication exceeds LimitToString `{context.LimitToString}`.");
}
```
```csharp
// src/Scriban/Functions/ArrayFunctions.cs:414 (array.insert_at — GUARDED, GHSA-24c8 fix in 7.2.0)
for (int i = array.Count; i < index; i++)
{
context.StepLoop(span, ref loopStep); // LoopLimit enforced
array.Add(null);
}
```
`array * int` (`ScriptArray.cs:504`) received neither guard.
When the oversized allocation fails as a managed exception, it is wrapped by the binary-expression evaluator:
```csharp
// src/Scriban/Syntax/Expressions/ScriptBinaryExpression.cs:241-243
catch (Exception ex) when (!(ex is ScriptRuntimeException))
{
throw new ScriptRuntimeException(span, ex.Message);
}
```
So a host that wraps `Render()` in `try/catch` sees a `ScriptRuntimeException` carrying the original `OutOfMemoryException` message (or `ArgumentOutOfRangeException` on the integer-overflow path).
### PoC
A single console project reproduces it on the released NuGet package.
`poc.csproj`:
```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<!-- If only the .NET 9 SDK is installed, change to net9.0. Behavior is identical. -->
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Scriban" Version="7.2.0" />
</ItemGroup>
</Project>
```
`Program.cs`:
```csharp
using Scriban;
// ~41-byte template requests 5 * 200,000,000 = 1,000,000,000 elements
string tpl = "{{ x = [1,2,3,4,5] * 200000000; x.size }}";
System.Console.WriteLine("Rendering...");
var sw = System.Diagnostics.Stopwatch.StartNew();
var result = Template.Parse(tpl).Render(); // allocates ~7.7 GB
System.Console.WriteLine($"size={result.Trim()} peakWS="
+ System.Diagnostics.Process.GetCurrentProcess().PeakWorkingSet64 / (1024 * 1024)
+ "MB elapsed=" + sw.ElapsedMilliseconds + "ms");
```
Run:
```sh
dotnet run -c Release
```
Measured peak working set on Scriban 7.2.0 (net8.0, .NET 9 runtime, Linux), varying only the multip
A null pointer dereference vulnerability exists in Magick.NET Q16 AnyCPU's distort operation when incorrect arguments are passed. This flaw can cause the application to crash, resulting in a denial of service. The issue affects versions prior to 14.14.0 and has been assigned CVE-2026-53463 with a medium severity rating.
Magick.NET Q16 AnyCPU versions prior to 14.14.0 contain a use-after-free vulnerability triggered when memory allocation fails in the CheckPrimitiveExtent function. This flaw can cause a heap-use-after-free condition leading to application crashes. The issue has been assigned CVE-2026-53462 and is addressed in version 14.14.0.
Magick.NET Q16 AnyCPU contains a vulnerability in its ICON image decoder where an incorrect loop causes an out-of-bounds heap write, leading to a crash. This flaw affects versions prior to 14.14.0 and has been assigned CVE-2026-53461. The issue is classified as a high severity vulnerability with a CVSS score of 7.5. A fix is available in version 14.14.0.
CVE-2026-53460 is a high-severity vulnerability in Magick.NET-Q16-AnyCPU where a missing check on maximum memory requests in the AcquireAlignedMemory function can lead to an out-of-memory condition. This flaw affects versions prior to 14.14.0 and has been fixed in version 14.14.0.
A vulnerability in Magick.NET-Q16-AnyCPU versions prior to 14.14.0 allows an attacker with local privileges to bypass security policies and read files disallowed by those policies via symlink manipulation. This is due to incorrect filename parsing. The issue is fixed in version 14.14.0.
A vulnerability in ImageMagick's DCM decoder allows an attacker to bypass policy checks, resulting in images with invalid dimensions. This flaw can cause crashes during subsequent image processing operations. The issue affects Magick.NET-Q16-AnyCPU versions prior to 14.14.0 and has been assigned CVE-2026-49218. A fix is available in version 14.14.0.
CVE-2026-48994 is a medium severity vulnerability in Magick.NET Q16 AnyCPU versions prior to 14.14.0. It involves a heap buffer over-write in the MAT decoder on 32-bit systems caused by a missing check of a return value. This flaw can lead to a denial of service due to application crash or instability. A patch is available in version 14.14.0.
Magick.NET Q16 AnyCPU contains a vulnerability in its MVG decoder where a crafted MVG file can trigger a stack overflow due to missing depth or visited-set checks. This flaw affects versions prior to 14.14.0 and has been assigned CVE-2026-48734. The vulnerability has a medium severity rating with a CVSS score of 5.5 and impacts availability without affecting confidentiality or integrity. A fix is available in version 14.14.0.
CVE-2026-48733 is a medium severity vulnerability in Magick.NET Q16 AnyCPU where a crafted image can trigger an infinite loop during the subimage-search operation. This flaw affects versions prior to 14.14.0 and can cause a denial of service by exhausting resources. A fix is available in version 14.14.0.
Magick.NET Q16 AnyCPU contains a heap buffer underwrite vulnerability in the Floyd-Steinberg depth dithering method when processing images with masks. This flaw can cause a negative heap buffer overwrite, potentially leading to application crashes or instability. The vulnerability affects versions prior to 14.14.0 and has been assigned CVE-2026-48724. A patch is available in version 14.14.0 to address this issue.
MessagePack-CSharp's typeless deserialization feature has a vulnerability where type restrictions do not recursively inspect array element types or generic type arguments. This allows an attacker to bypass outer-type blocklist checks by wrapping disallowed types inside arrays or generic containers. The issue affects applications that deserialize untrusted data using typeless serialization APIs. Fixes are available in versions 2.5.301 and 3.1.7. Users are advised to upgrade and avoid typeless deserialization of untrusted data or use explicit recursive allowlists.
MessagePack-CSharp's InterfaceLookupFormatter<TKey,TElement> constructs an internal dictionary using the default equality comparer instead of the security-aware comparer when deserializing ILookup<TKey,TElement>. This omission allows an attacker to craft payloads with colliding keys that degrade dictionary insertion performance, causing a CPU denial of service even when the application opts into untrusted-data security settings. The vulnerability affects versions of MessagePack prior to 2.5.301 and versions 3.0 up to but not including 3.1.7.
MessagePack-CSharp contains a vulnerability in its multi-dimensional array formatters where dimension lengths are read from untrusted payloads and used to allocate arrays before validating that the total element count matches the encoded data. This can lead to excessive memory allocation and potential out-of-memory conditions when deserializing untrusted data into multi-dimensional arrays such as T[,], T[,,], or T[,,,]. The issue affects versions prior to 2.5.301 and versions 3.0 up to but not including 3.1.7. Fixes are prepared but not yet released. Until patched, users should avoid deserializing untrusted payloads into multi-dimensional arrays and prefer safer data shapes.
MessagePack-CSharp's UnsafeBlitFormatterBase<T>.Deserialize method in Unity blit resolvers improperly allocates memory based on an attacker-controlled byte length without validating it against the actual payload size. This can lead to excessive memory allocation and potential out-of-memory exceptions or process termination on memory-constrained platforms when deserializing untrusted data. The vulnerability affects the MessagePack.UnityClient package and specific resolvers prior to patched versions. The issue is mitigated by upgrading to patched versions or avoiding use of vulnerable resolvers with untrusted input.
CVE-2026-56370 is a medium severity vulnerability in ImageMagick's Magick.NET Q16 AnyCPU component. It involves an out-of-bounds access in the ConnectedComponentsImage() function triggered by an invalid index in the connected-components:* define, leading to an access violation. This vulnerability affects multiple specific versions of ImageMagick prior to 7.1.2-19. A patch is available to address this issue.
A stored cross-site scripting (XSS) vulnerability exists in the media plugin of TinyMCE. Attackers can inject malicious scripts via crafted data-mce-* attributes, which execute when the content is rendered. This affects users of TinyMCE with the media plugin enabled. The vulnerability has been patched in TinyMCE versions 8.5.1, 7.9.3, and 5.11.1 LTS. No official workaround is available other than upgrading to a fixed version.