Scriban: Template Writes to Arbitrary CLR Properties via `TypedObjectAccessor` (Mass Assignment + `private` / `init` / `internal` Setter Bypass)
<!-- 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(
AI Analysis
Technical Summary
When a CLR object is pushed into a Scriban TemplateContext, TypedObjectAccessor exposes all public-getter properties for reading and writing without validating setter accessibility. This leads to two related issues: (A) mass assignment of any property with a public setter, allowing template code to modify such properties; and (B) bypass of access modifiers for private, internal, and init-only setters, permitting template code to write to properties that developers intended to restrict. The vulnerability affects all versions shipping TypedObjectAccessor (<=7.2.1). There is no official patch yet, but a proposed fix involves blocking restricted setters in the TrySetValue method by requiring a public, non-init setter.
Potential Impact
An attacker able to influence Scriban template code can modify live CLR objects' properties, including those intended to be immutable or restricted, potentially leading to unauthorized state changes or privilege escalation within the host application. These changes persist after template rendering completes, affecting application behavior or data integrity.
Mitigation Recommendations
No official patch or fix is currently available. Users should monitor vendor advisories for updates. The vulnerability requires code changes in TypedObjectAccessor to block writes to non-public and init-only setters. Until a fix is released, avoid pushing untrusted CLR objects into Scriban TemplateContext or restrict template code execution privileges.
Scriban: Template Writes to Arbitrary CLR Properties via `TypedObjectAccessor` (Mass Assignment + `private` / `init` / `internal` Setter Bypass)
Description
<!-- 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(
CVSS v4.0
Affected software
Run on your own infrastructure? Check whether these packages are installed with threat-finder — our free open-source scanner.
AI-Powered Analysis
Machine-generated threat intelligence
Technical Analysis
When a CLR object is pushed into a Scriban TemplateContext, TypedObjectAccessor exposes all public-getter properties for reading and writing without validating setter accessibility. This leads to two related issues: (A) mass assignment of any property with a public setter, allowing template code to modify such properties; and (B) bypass of access modifiers for private, internal, and init-only setters, permitting template code to write to properties that developers intended to restrict. The vulnerability affects all versions shipping TypedObjectAccessor (<=7.2.1). There is no official patch yet, but a proposed fix involves blocking restricted setters in the TrySetValue method by requiring a public, non-init setter.
Potential Impact
An attacker able to influence Scriban template code can modify live CLR objects' properties, including those intended to be immutable or restricted, potentially leading to unauthorized state changes or privilege escalation within the host application. These changes persist after template rendering completes, affecting application behavior or data integrity.
Mitigation Recommendations
No official patch or fix is currently available. Users should monitor vendor advisories for updates. The vulnerability requires code changes in TypedObjectAccessor to block writes to non-public and init-only setters. Until a fix is released, avoid pushing untrusted CLR objects into Scriban TemplateContext or restrict template code execution privileges.
Technical Details
- Gcve Source
- db.gcve.eu
- Osv Id
- GHSA-7jvp-hj45-2f2m
- Osv Schema Version
- 1.4.0
- Aliases
- []
- Ecosystems
- ["NuGet"]
- Database Specific Severity
- HIGH
- Cvss Version
- 4.0
Threat ID: 6a4c345427e9c797195fe153
Added to database: 07/06/2026, 23:03:48 UTC
Last enriched: 07/06/2026, 23:36:26 UTC
Last updated: 07/31/2026, 14:54:39 UTC
Views: 52
Community Reviews
0 reviewsCrowdsource mitigation strategies, share intel context, and vote on the most helpful responses. Sign in to add your voice and help keep defenders ahead.
Want to contribute mitigation steps or threat intel context? Sign in or create an account to join the community discussion.
Actions
Updates to AI analysis require Pro Console access. Upgrade inside Console → Billing.
External Links
Need more coverage?
Upgrade to Pro Console for AI refresh and higher limits.
For incident response and remediation, OffSeq services can help resolve threats faster.
Latest Threats
Check if your credentials are on the dark web
Instant breach scanning across billions of leaked records. Free tier available.