Vulnerabilities in pkg:nuget/Scriban
PACKAGEGHSA-7jvp-hj45-2f2m is a HIGH template writes to arbitrary clr vulnerability (CWE-284: Improper Access Control) affecting Scriban libraries in the NuGet ecosystem. The issue involves template writes to arbitrary clr properties via `typedobjectaccessor` (mass assignment + `private` / `init` / `internal` setter bypass). Remediation is available by upgrading affected packages to version 7.2.2.
pkg:nuget/Scriban
Vulnerability advisories affecting package pkg:nuget/Scriban. Select an advisory below to view technical details or fixes.
| Advisory ID | CVE / NVD | Severity | Summary |
|---|---|---|---|
| GHSA-7jvp-hj45-2f2m | - | HIGH | Scriban: Template Writes to Arbitrary CLR Properties via `TypedObjectAccessor` (Mass Assignment + `private` / `init` / `internal` Setter Bypass) |
| GHSA-6q7j-xr26-3h2c | - | MODERATE | Scriban: ExpressionDepthLimit guard is non-enforcing — parser-recursion DoS in 6.6.0–7.2.0 (incomplete fix for GHSA-wgh7-7m3c-fx25 / GHSA-p6q4-fgr8-vx4p) |
| GHSA-xw6w-9jjh-p9cr | - | MODERATE | Scriban has Multiple Denial-of-Service Vectors via Unbounded Resource Consumption During Expression Evaluation |
| GHSA-m2p3-hwv5-xpqw | - | MODERATE | Scriban: Denial of Service via Unbounded Cumulative Template Output Bypassing LimitToString |
| GHSA-xcx6-vp38-8hr5 | - | HIGH | Scriban has Uncontrolled Recursion in `object.to_json` Causing Unrecoverable Process Crash via StackOverflowException |
| GHSA-v66j-x4hw-fv9g | - | HIGH | Scriban: Uncontrolled Memory Allocation via string.pad_left/pad_right Allows Remote Denial of Service |
| GHSA-p6q4-fgr8-vx4p | - | HIGH | Scriban has a Stack Overflow via Nested Array Initializers That Bypass the ExpressionDepthLimit Fix |
| GHSA-q6rr-fm2g-g5x8 | - | MODERATE | Scriban: array * int (ScriptArray<T>.TryEvaluate) bypasses LoopLimit — incomplete fix for GHSA-c875-h985-hvrc, missed sibling of GHSA-24c8-4792-22hx |
GHSA-7jvp-hj45-2f2m at a Glance
How to Fix GHSA-7jvp-hj45-2f2m
To remediate this vulnerability, upgrade all affected Scriban dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
Upgrade Scriban to 7.2.2
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
inspect dependencies for Scriban
How to Check if You Are Affected
To determine whether your application is affected by GHSA-7jvp-hj45-2f2m, inspect your project dependencies for Scriban:
inspect dependencies for Scriban
Analyze the terminal output to locate any version of Scriban in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 7.2.2), your application is affected.
Note: Verify declared build manifests and deployed runtime container environments.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-284: Improper Access Control, where the product does not restrict or incorrectly restricts access to a resource from an unauthorized actor. In Scriban, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
GHSA-7jvp-hj45-2f2m Affected Scriban Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
Scriban
|
NuGet |
< 7.2.2
|
7.2.2
|
References
Frequently Asked Questions
What is GHSA-7jvp-hj45-2f2m?
Which Scriban versions are affected?
What version fixes GHSA-7jvp-hj45-2f2m?
Is Scriban affected?
What CWE is associated with GHSA-7jvp-hj45-2f2m?
How can I check NuGet dependencies for GHSA-7jvp-hj45-2f2m?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-7jvp-hj45-2f2m",
"modified" : "2026-07-06T17:30:17Z",
"published" : "2026-07-06T17:30:17Z",
"aliases" : [ ],
"summary" : "Scriban: Template Writes to Arbitrary CLR Properties via `TypedObjectAccessor` (Mass Assignment + `private` / `init` / `internal` Setter Bypass)",
"details" : "<!-- obsidian --><h2 data-heading=\"Description\">Description</h2>\n<p>When a host pushes a CLR object into a Scriban <code>TemplateContext</code> via the standard, documented pattern —</p>\n<pre><code class=\"language-csharp\">var so = new ScriptObject();\nso[\"user\"] = currentUser; // direct CLR reference\ncontext.PushGlobal(so);\n</code></pre>\n<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>\n<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>\n<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>\n\nDeclaration | Developer intent | Actual behavior\n-- | -- | --\n{ get; set; } | writable | writable (mass assignment — A)\n{ get; private set; } | only the owning class writes | template writes freely\n{ get; internal set; } | only the declaring assembly writes | template writes freely\n{ get; init; } | immutable after construction (C# 9 language guarantee) | template writes freely post-construction\n\n\n<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>\n<h2 data-heading=\"Affected Versions\">Affected Versions</h2>\n<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>\n<h2 data-heading=\"Steps to Reproduce\">Steps to Reproduce</h2>\n<blockquote>\n<p>Copy-paste. Run from the engagement root (the folder containing both <code>scriban/</code> and <code>reports/</code>).</p>\n</blockquote>\n<p><strong>Prereqs:</strong></p>\n<pre><code class=\"language-bash\">test -d scriban || { echo \"scriban source missing\"; exit 1; }\n( command -v dotnet >/dev/null && dotnet --list-sdks | grep -q '^10\\.' ) \\\n || ( \"$HOME/.dotnet/dotnet\" --list-sdks | grep -q '^10\\.' ) \\\n || { echo \".NET 10 SDK missing\"; exit 1; }\nexport PATH=\"$HOME/.dotnet:$PATH\"\n</code></pre>\n<p><strong>Run both PoCs (native):</strong></p>\n<pre><code class=\"language-bash\">( cd reports/f002/poc && dotnet run -c Release ) # (A) public-setter mass assignment\n( cd reports/f007/poc && dotnet run -c Release ) # (B) private/internal/init bypass\n</code></pre>\n<p><strong>Docker fallback (no native SDK required):</strong></p>\n<pre><code class=\"language-bash\">docker run --rm -v \"$PWD\":/work -w /work/reports/f007/poc \\\n mcr.microsoft.com/dotnet/sdk:10.0 bash -lc \"dotnet run -c Release\"\n</code></pre>\n<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>\n<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>\n<h2 data-heading=\"Remediation\">Remediation</h2>\n<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>\n<ul>\n<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:\n<pre><code class=\"language-csharp\">var setM = propertyAccessor.GetSetMethod(nonPublic: false);\nif (setM is null) return false; // private / internal / protected setters\nif (setM.ReturnParameter.GetRequiredCustomModifiers()\n .Any(m => m.FullName == \"System.Runtime.CompilerServices.IsExternalInit\"))\n return false; // init-only: setter IS public, so the IsExternalInit check is REQUIRED\n</code></pre>\nA plain <code>GetSetMethod(nonPublic:false) != null</code> check is <strong>not</strong> sufficient for <code>init</code> — the init setter is public; only the <code>IsExternalInit</code> modreq distinguishes it.</li>\n<li><strong>Fix 2 — give hosts a read/write distinction (addresses (A)).</strong> Add a <code>MemberWriteFilter</code> on <code>TemplateContext</code> (separate from <code>MemberFilter</code>) and/or a <code>[ScriptMemberReadOnly]</code> attribute, and split <code>_members</code> into <code>_readableMembers</code> / <code>_writableMembers</code> in <code>PrepareMembers</code> (L126–L186). Public-settable mass assignment cannot be blocked without one of these, because <code>MemberFilter</code> is read/write-symmetric today.</li>\n<li><strong>Fix 3 — restore read-only-by-default on <code>ScriptObject.Import</code> (<code>ScriptObjectExtensions.cs</code> L320–L324).</strong> Gate the Liquid-compatibility relaxation behind an explicit opt-in instead of removing write protection globally.</li>\n<li><strong>Fix 4 — documentation (<code>site/docs/runtime/safe-runtime.md</code>).</strong> State explicitly that templates can write CLR properties via reflection (including <code>private</code>/<code>internal</code>/<code>init</code> setters), and that <code>MemberFilter</code> does not separate read from write.</li>\n<li><strong>Fix 5 — regression tests (<code>src/Scriban.Tests/</code>).</strong> Assert <code>private set</code> / <code>internal set</code> / <code>init</code> are non-writable from templates, that <code>MemberWriteFilter</code> / <code>[ScriptMemberReadOnly]</code> gate writes, and that only public <code>set</code> is writable.</li>\n</ul>\n<h2 data-heading=\"References\">References</h2>\n<ul>\n<li>Vulnerable write path (no setter check): <code>scriban/src/Scriban/Runtime/Accessors/TypedObjectAccessor.cs</code> L108–L123 (<code>TrySetValue</code>), sink at L120 <code>propertyAccessor.SetValue(target, context.ToObject(span, value, propertyAccessor.PropertyType));</code></li>\n<li>Getter-only member filter: <code>TypedObjectAccessor.cs</code> L126–L186 (<code>PrepareMembers</code>), enumeration at L150, gate at L156; same <code>_members</code> consumed by <code>TryGetValue</code> (L66–L83)</li>\n<li>Member-assignment dispatch: <code>scriban/src/Scriban/ScribanAsync.generated.cs:2297</code> (<code>accessor.TrySetValue(...)</code>) and the synchronous evaluator</li>\n<li>No read/write separation: <code>MemberFilter</code> declared <code>TemplateContext.cs:286</code>, applied <code>TemplateContext.cs:1026</code>; <code>ScriptObject.Import</code> read-only removal <code>ScriptObjectExtensions.cs:320–324</code></li>\n<li>.NET reflection bypasses access modifiers: <a href=\"https://learn.microsoft.com/dotnet/api/system.reflection.propertyinfo.setvalue\" class=\"external-link\" target=\"_blank\" rel=\"noopener nofollow\">https://learn.microsoft.com/dotnet/api/system.reflection.propertyinfo.setvalue</a></li>\n<li><code>init</code> accessors (C# 9): <a href=\"https://learn.microsoft.com/dotnet/csharp/language-reference/proposals/csharp-9.0/init\" class=\"external-link\" target=\"_blank\" rel=\"noopener nofollow\">https://learn.microsoft.com/dotnet/csharp/language-reference/proposals/csharp-9.0/init</a></li>\n<li>CWE-915 — <a href=\"https://cwe.mitre.org/data/definitions/915.html\" class=\"external-link\" target=\"_blank\" rel=\"noopener nofollow\">https://cwe.mitre.org/data/definitions/915.html</a></li>\n<li>CWE-284 — <a href=\"https://cwe.mitre.org/data/definitions/284.html\" class=\"external-link\" target=\"_blank\" rel=\"noopener nofollow\">https://cwe.mitre.org/data/definitions/284.html</a></li>\n</ul>",
"severity" : [ {
"type" : "CVSS_V4",
"score" : "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N/E:P"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "NuGet",
"name" : "Scriban"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "7.2.2"
} ]
} ],
"database_specific" : {
"last_known_affected_version_range" : "<= 7.2.1"
}
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/scriban/scriban/security/advisories/GHSA-7jvp-hj45-2f2m"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/scriban/scriban"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-284", "CWE-915" ],
"severity" : "HIGH",
"github_reviewed" : true,
"github_reviewed_at" : "2026-07-06T17:30:17Z",
"nvd_published_at" : null
}
}