{"id":"GHSA-xw6w-9jjh-p9cr","summary":"Scriban has Multiple Denial-of-Service Vectors via Unbounded Resource Consumption During Expression Evaluation","details":"## Summary\n\nScriban'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.\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 — `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` — 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 — String multiplication OOM:\n```csharp\nvar template = Template.Parse(\"{{ 'AAAA' * 500000000 }}\");\nvar context = new TemplateContext();\n// context.LimitToString is 1048576 by default — does NOT protect this path\ntemplate.Render(context); // OutOfMemoryException: attempts ~2GB allocation\n```\n\n### Vector 2 — 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 — LoopLimit bypass via range + builtin:\n```csharp\nvar template = Template.Parse(\"{{ (0..1000000000) | array.size }}\");\nvar context = new TemplateContext();\n// context.LoopLimit is 1000 — 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 ',' }}\");\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 — 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 — 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 && (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 — 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 — 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 ...`","modified":"2026-03-24T22:39:45.198987Z","published":"2026-03-24T22:16:01Z","database_specific":{"cwe_ids":["CWE-400"],"github_reviewed_at":"2026-03-24T22:16:01Z","severity":"MODERATE","github_reviewed":true,"nvd_published_at":null},"references":[{"type":"WEB","url":"https://github.com/scriban/scriban/security/advisories/GHSA-xw6w-9jjh-p9cr"},{"type":"PACKAGE","url":"https://github.com/scriban/scriban"}],"affected":[{"package":{"name":"Scriban","ecosystem":"NuGet","purl":"pkg:nuget/Scriban"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"7.0.0"}]}],"versions":["0.1.0","0.10.0","0.11.0","0.12.0","0.12.1","0.13.0","0.14.0","0.15.0","0.16.0","0.2.0","0.2.1","0.2.2","0.3.0","0.3.1","0.3.1-pre028","0.4.0","0.5.0","0.6.0","0.7.0","0.9.0","0.9.0-pre100","0.9.1","1.0.0","1.0.0-beta-001","1.0.0-beta-002","1.0.0-beta-003","1.0.0-beta-004","1.0.0-beta-005","1.0.0-beta-006","1.1.0","1.1.1","1.2.0","1.2.1","1.2.2","1.2.3","1.2.4","1.2.5","1.2.6","1.2.7","1.2.8","1.2.9","2.0.0","2.0.0-alpha-001","2.0.0-alpha-002","2.0.0-alpha-003","2.0.0-alpha-004","2.0.0-alpha-005","2.0.0-alpha-006","2.0.1","2.1.0","2.1.1","2.1.2","2.1.3","2.1.4","3.0.0","3.0.1","3.0.2","3.0.3","3.0.4","3.0.5","3.0.6","3.0.7","3.1.0","3.2.0","3.2.1","3.2.2","3.3.0","3.3.1","3.3.2","3.3.3","3.4.0","3.4.1","3.4.2","3.5.0","3.6.0","3.7.0","3.8.0","3.8.1","3.8.2","3.9.0","4.0.0","4.0.1","4.0.2","4.1.0","5.0.0","5.1.0","5.10.0","5.11.0","5.12.0","5.12.1","5.2.0","5.3.0","5.4.0","5.4.1","5.4.2","5.4.3","5.4.4","5.4.5","5.4.6","5.5.0","5.5.1","5.5.2","5.6.0","5.7.0","5.8.0","5.9.0","5.9.1","6.0.0","6.1.0","6.2.0","6.2.1","6.3.0","6.4.0","6.5.0","6.5.1","6.5.2","6.5.3","6.5.4","6.5.5","6.5.6","6.5.7","6.5.8","6.6.0"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/03/GHSA-xw6w-9jjh-p9cr/GHSA-xw6w-9jjh-p9cr.json"}}],"schema_version":"1.7.5","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H"}]}