Bump dependency Scriban to v7 [SECURITY]#176
Open
renovate[bot] wants to merge 1 commit into
Open
Conversation
5b8b9c8 to
9f02510
Compare
9f02510 to
2dd4298
Compare
2dd4298 to
a080e1b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains the following updates:
6.4.0→7.0.0Scriban has Uncontrolled Recursion in Parser Leads to Stack Overflow and Process Crash (Denial of Service)
GHSA-wgh7-7m3c-fx25
More information
Details
Scriban is vulnerable to an uncontrolled process crash resulting in a Denial of Service. Because the recursive-descent parser does not enforce a default limit on expression depth, an attacker who controls template input can craft a heavily nested template that triggers a
StackOverflowException. In .NET, aStackOverflowExceptioncannot be caught by standardtry-catchblocks, resulting in the immediate and ungraceful termination of the entire hosting process.Scriban utilizes a recursive-descent parser to process template expressions. While the library exposes an
ExpressionDepthLimitproperty in itsParserOptions, this property defaults tonull(disabled).If an application accepts user-supplied templates (or dynamically constructs templates from untrusted input), an attacker can supply thousands of nested parentheses or blocks. As the parser recursively evaluates each nested layer, it consumes thread stack space until it exceeds the limits of the host OS, triggering a fatal crash.
Impact
An attacker can supply crafted input that triggers a
StackOverflowException, causing immediate termination of the hosting process and resulting in a Denial of Service. In applications that process untrusted or user-controlled templates (e.g., web applications or APIs), this can be exploited remotely without authentication. The failure is not recoverable, requiring a full process restart and leading to service disruption.Proof of Concept (PoC)
The following C# code demonstrates the vulnerability. Executing this code will immediately terminate the application process.
Suggested Remediation
Update the
ParserOptionsconstructor (or the internal parser initialization) to set a default value forExpressionDepthLimit. A limit of1000(or even lower, such as250or500) is generally more than enough for legitimate templates while safely preventing stack exhaustion.Alternatively, document the risk heavily and warn developers to manually set
ExpressionDepthLimitif evaluating untrusted templates, though a secure-by-default approach is strongly preferred.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Scriban has an Infinite Recursion during Object Rendering Leads to Stack Overflow and Process Crash (Denial of Service)
GHSA-grr9-747v-xvcp
More information
Details
When Scriban renders an object that contains a circular reference, it traverses the object's members infinitely. Because the
ObjectRecursionLimitproperty defaults to unlimited, this behavior exhausts the thread's stack space, triggering an uncatchableStackOverflowExceptionthat immediately terminates the hosting process.When rendering objects (e.g., ``), the Scriban rendering engine recursively inspects and formats the object's properties. To prevent infinite loops caused by deeply nested or circular data structures,
TemplateContextcontains an `ObjectRecursionLimit` property.However, this property currently defaults to
0(unlimited). If the data context pushed into the template contains a circular reference, the renderer will recurse indefinitely. This is especially dangerous for web applications that map user-controlled payloads (like JSON) directly to rendering contexts, or for applications that pass ORM objects (like Entity Framework models, which frequently contain circular navigation properties) into the template.Proof of Concept (PoC)
The following C# code demonstrates the vulnerability. Executing this will cause an immediate, fatal
StackOverflowException, bypassing any standard error handling.Impact
This vulnerability allows a Denial of Service (DoS) attack. If a malicious user can manipulate the data structure passed to the renderer to include a cyclic reference, or if the application passes a complex object graph to an untrusted template, the entire .NET hosting process will crash.
Suggested Remediation
Update
TemplateContext.csto setObjectRecursionLimitto a safe default, such as20.By implementing this default, circular references will gracefully result in a catchable
ScriptRuntimeExceptionrather than a fatal process crash.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Scriban Affected by Memory Exhaustion (OOM) via Unbounded String Generation (Denial of Service)
GHSA-5rpf-x9jg-8j5p
More information
Details
TemplateContext.LimitToStringdefaults to0(unlimited). While Scriban implements a defaultLoopLimitof 1000, an attacker can still cause massive memory allocation via exponential string growth. Doubling a string for just 30 iterations generates over 1GB of text, instantly exhausting heap memory and crashing the host process. Because no output size limit is enforced, repeated string concatenation results in exponential memory growth.Proof of Concept (PoC):
The following payload executes in under 30 iterations but results in ~1GB string allocation, crashing the process.
Impact:
An attacker can supply a small template that triggers exponential string growth, forcing the application to allocate excessive memory. This leads to severe memory pressure, garbage collection thrashing, and eventual process termination (DoS).
Suggested Fix:
Enforce a sensible default limit for string output. Set default
LimitToStringto 1MB (1,048,576 characters).Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:LReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Scriban has an authorization bypass due to stale include cache surviving TemplateContext.Reset()
GHSA-x6m9-38vm-2xhf
More information
Details
Summary
TemplateContext.Reset()claims that aTemplateContextcan be reused safely on the same thread, but it does not clearCachedTemplates. If an application poolsTemplateContextobjects and uses anITemplateLoaderthat resolves content per request, tenant, or user, a previously authorized include can be served to later renders without callingTemplateLoader.Load()again.Details
The relevant code path is:
TemplateContext.Reset()only clears output, globals, cultures, and source files insrc/Scriban/TemplateContext.cslines 877–902.CachedTemplatesis initialized once and kept on the context insrc/Scriban/TemplateContext.csline 197.includeresolves templates throughIncludeFunction.Invoke()insrc/Scriban/Functions/IncludeFunction.cslines 29–43.IncludeFunction.Invoke()callsTemplateContext.GetOrCreateTemplate()insrc/Scriban/TemplateContext.cslines 1249–1256.CachedTemplates, Scriban returns the cached compiled template and does not callTemplateLoader.Load()again.This becomes a security issue when
ITemplateLoader.Load()returns request-dependent content. A first render can prime the cache with an admin-only or tenant-specific template, and later renders on the same reusedTemplateContextwill receive that stale template even afterReset().Proof of Concept
Setup
mkdir scriban-poc1 cd scriban-poc1 dotnet new console --framework net8.0 dotnet add package Scriban --version 6.6.0Program.csRun
Actual Output
Expected Output
The second render should reload the template after
Reset(), but it instead reuses the cached compiled template from the previous render.Impact
This is a cross-render data isolation issue. Any application that reuses
TemplateContextobjects and uses a request-dependentITemplateLoadercan leak previously authorized template content across requests, users, or tenants.The issue impacts applications that:
TemplateContextReset()between requestsincludeSeverity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Scriban: Sandbox escape due to TypedObjectAccessorcache bypassing MemberFilter after TemplateContext reuse
GHSA-5wr9-m6jw-xx44
More information
Details
Summary
TemplateContextcaches type accessors byTypeonly, but those accessors are built using the currentMemberFilterandMemberRenamer. When aTemplateContextis 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.Details
The relevant code path is:
TemplateContext.GetMemberAccessor()caches accessors in_memberAccessorsbyTypeinsrc/Scriban/TemplateContext.cslines 850–863.GetMemberAccessorImpl()creates a newTypedObjectAccessor(type, _keyComparer, MemberFilter, MemberRenamer)insrc/Scriban/TemplateContext.cslines 909–939.TypedObjectAccessorstores the current filter and precomputes the exposed member set in its constructor andPrepareMembers()insrc/Scriban/Runtime/Accessors/TypedObjectAccessor.cslines 33–40 and 119–179.ScriptMemberExpression.GetValue()insrc/Scriban/Syntax/Expressions/ScriptMemberExpression.cslines 67–95, which uses the cached accessor.TemplateContext.Reset()does not clear_memberAccessorsinsrc/Scriban/TemplateContext.cslines 877–902.As a result, once a permissive accessor has been created for a given type, changing
TemplateContext.MemberFilterlater does not take effect for that type on the same reused context.This is especially relevant because the Scriban docs explicitly recommend
TemplateContext.MemberFilterfor indirect .NET object exposure.Proof of Concept
Setup
mkdir scriban-poc2 cd scriban-poc2 dotnet new console --framework net8.0 dotnet add package Scriban --version 6.6.0Program.csRun
Actual Output
Expected Behavior
The second render should fail or stop exposing
Secret, because the filter only allowsPublicandEnableRelaxedMemberAccessis disabled.This reproduces a direct filter bypass caused by the stale cached accessor.
Impact
This is a protection-mechanism bypass. Applications that use
TemplateContext.MemberFilteras part of their sandbox or object-exposure policy can unintentionally expose hidden members across requests when they reuse aTemplateContext.The impact includes:
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Scriban: Built-in operations bypass LoopLimit and delay cancellation, enabling Denial of Service
GHSA-c875-h985-hvrc
More information
Details
Summary
Scriban's
LoopLimitonly 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 whenLoopLimitis set to a very small value.Details
The relevant code path is:
ScriptBlockStatement.Evaluate()callscontext.CheckAbort()once per statement insrc/Scriban/Syntax/Statements/ScriptBlockStatement.cslines 41–46.LoopLimitenforcement is tied to script loop execution viaTemplateContext.StepLoop(), not to internal helper iteration.array.sizeinsrc/Scriban/Functions/ArrayFunctions.cslines 596–609 callslist.Cast<object>().Count()for non-collection enumerables.1..Ncreates aScriptRangefromScriptBinaryExpression.RangeInclude()insrc/Scriban/Syntax/Expressions/ScriptBinaryExpression.cslines 745–748.ScriptRangethen yields every element one by one without going throughStepLoop()insrc/Scriban/Runtime/ScriptRange.cs.This means a single statement can perform arbitrarily large iteration without being stopped by
LoopLimit.There is also a related memory-amplification path in
string * int:ScriptBinaryExpression.CalculateToString()appends in a plainforloop insrc/Scriban/Syntax/Expressions/ScriptBinaryExpression.cslines 301–334.Proof of Concept
Setup
mkdir scriban-poc3 cd scriban-poc3 dotnet new console --framework net8.0 dotnet add package Scriban --version 6.6.0Program.csRun
Actual Output
Expected Behavior
A safety limit of
LoopLimit = 1should prevent a template from performing one million iterations worth of work.Optional Stronger Variant (Memory Amplification)
This variant demonstrates that
LoopLimitalso does not constrain large internal allocation work.Impact
This is an uncontrolled resource consumption issue. Any application that accepts attacker-controlled templates and relies on
LoopLimitas part of its safe-runtime configuration can still be forced into heavy CPU or memory work by a single expression.The issue impacts:
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Scriban: array.insert_at index parameter DoS bypasses LoopLimit and LimitToString
GHSA-24c8-4792-22hx
More information
Details
Summary
ArrayFunctions.InsertAtin Scriban allocatesindex - list.Countnull entries in a tight C#forloop with no bound onindex. The function is exposed to template authors asarray.insert_at, and the fill loop ignores every existing safety control:LoopLimit,LimitToString,ObjectRecursionLimit, andRecursiveLimit. A single template such as{{ [1] | array.insert_at 200000000 'x' | array.size }}causesOutOfMemoryExceptionin well under a second on a host with 1 GB of memory, even whenLoopLimitis set to10andLimitToStringis set to100. BecauseOutOfMemoryExceptionis generally not caught by the template renderer or by typical host applications, the vulnerability terminates the host process, not just the template.This is a sibling vector to GHSA-xw6w-9jjh-p9cr / GHSA-c875-h985-hvrc / GHSA-v66j-x4hw-fv9g, which patched comparable unbounded primitives in
string * int,array.size,array.join,string.pad_left, andstring.pad_right. The 7.0.0 hardening pass (dde661d"Apply LoopLimit to internal iteration paths" and4227fde"Harden string padding width limits") swept the equivalent loops inArrayFunctionsandStringFunctionsbut missedInsertAt.Details
Reproducible in 7.1.0 (latest tag) and on
masteratc8094b0.src/Scriban/Functions/ArrayFunctions.cs:369-386:The function is registered as the template builtin
array.insert_at(array.fmt-csand the standardArrayFunctionsScriptObject reflection registration). It is invoked from a template like[1] | array.insert_at 999999999 "x".Three properties combine to make this exploitable:
There is no context-aware overload. Comparable amplification primitives in this same file received a
(TemplateContext, SourceSpan, ...)overload that callsStepLoopper iteration (AddRange,Compact,Concat,Last,Limit,Offset,Reverse,Size,Sort,Uniq,Contains,Each,Filter,Join,Map,Any-- see commitdde661d).InsertAtwas not given that treatment. The singleIEnumerable, int, objectsignature is what the engine resolves to, so no host configuration changes its behaviour.The loop itself never consults
context.LoopLimit,context.LimitToString,context.RecursiveLimit, orcontext.ObjectRecursionLimit. There is no upstream call intocontext.StepLoop,context.CheckAbort, or any guard. Withindex = 200_000_000, the C# loop callsScriptArray.Add(null)200 million times on aList<object>whose capacity doubles geometrically; the JIT-compiled tight loop reaches the .NET array allocator faster than the GC can keep up.OutOfMemoryExceptionis the actual failure mode. Per Microsoft,OutOfMemoryExceptionand friends are not reliably catchable by user code in production CLR runtimes; even when they are caught, large background allocations and triggered GC cycles leave the process in a degraded state. In the PoC below, the renderer wraps the OOM in aScriptRuntimeExceptionbecause the underlying allocation lands inside the renderer's try block, but on hosts that allocate the array slightly differently (e.g. tighter memory cap, server GC, or higher index value than the host has memory for) the bareOutOfMemoryExceptionpropagates and crashes the AppDomain.The pattern that matches the existing fixes is to add a context-aware overload that validates
indexagainstLoopLimit(orLimitToStringfor the resulting array footprint) before the fill loop runs, and to mark the unsafe overload[ScriptMemberIgnore]:Same pattern as
ArrayFunctions.AddRange,Compact,Concat,Last,Limit, etc., introduced bydde661d, and asStringFunctions.PadLeft/PadRightintroduced by4227fde.PoC
Standalone .NET 9 console app referencing
Scriban7.1.0 from NuGet.poc.csproj:Program.cs:Build and run inside a memory-capped Docker container so the OOM is actual, not theoretical:
docker run --rm -v "$PWD":/app -w /app -m 1g mcr.microsoft.com/dotnet/sdk:9.0 \ dotnet run -c ReleaseObserved output:
Two observations:
LoopLimit = 10andLimitToString = 100(effectively the most paranoid tuning a host could pick) makes no difference. The fill loop is in compiled C#, never goes throughStepLoop, and the result is aScriptArray, not a string, soLimitToStringis never consulted.Impact
Denial of service against any host that renders attacker-controlled or attacker-influenced Scriban templates. This includes the canonical Scriban use cases the README itself lists -- email templating, report templating, in-CMS templating, and Statiq-style static site generators where the template content is part of the data ingested. A single one-line template payload is enough to either OOM the process outright (when the host gives the renderer enough memory headroom for the loop to actually finish) or to wedge the process for tens of seconds while the allocator and GC fight (when memory is tight). On ASP.NET hosts using
app.UseScriban-style middleware or background workers running per-tenant templates, the OOM terminates the entire process, taking down all tenants.Severity is consistent with the four DoS GHSAs already published against Scriban (
GHSA-xw6w-9jjh-p9crHigh 7.5,GHSA-c875-h985-hvrcHigh 7.5,GHSA-v66j-x4hw-fv9gHigh 7.5,GHSA-m2p3-hwv5-xpqwHigh 7.5). The attack vector, complexity, and impact are identical: network reachable, low complexity, no privileges, no user interaction, full availability impact, no confidentiality or integrity impact. CVSS 4.0 vector: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(High, 8.7).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:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Scriban has a Stack Overflow via Nested Array Initializers That Bypass the ExpressionDepthLimit Fix
GHSA-p6q4-fgr8-vx4p
More information
Details
Summary
StackOverflowException via nested array initializers bypasses ExpressionDepthLimit fix (GHSA-wgh7-7m3c-fx25)
Details
The recent fix for GHSA-wgh7-7m3c-fx25 (uncontrolled recursion in parser) added
ExpressionDepthLimitdefaulting to 250. However, deeply nested array initializers ([[[[...) recurse throughParseArrayInitializer→ParseExpression→ParseArrayInitializer, which is a different recursion path not covered by the expression depth counter.This causes a
StackOverflowExceptionon current main (commit b5ac4bf - "Add limits for default safety").PoC
Impact
Same as GHSA-wgh7-7m3c-fx25: High severity. StackOverflowException cannot be caught with try/catch in .NET - the process terminates immediately. Any application calling Template.Parse with untrusted input is vulnerable, even with the new default ExpressionDepthLimit enabled.
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Scriban: Uncontrolled Memory Allocation via string.pad_left/pad_right Allows Remote Denial of Service
GHSA-v66j-x4hw-fv9g
More information
Details
Summary
The built-in
string.pad_leftandstring.pad_righttemplate functions in Scriban perform no validation on thewidthparameter, 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 viaOutOfMemoryException.Details
StringFunctions.PadLeftandStringFunctions.PadRight(src/Scriban/Functions/StringFunctions.cs:1181-1203) directly delegate to .NET'sString.PadLeft(int)/String.PadRight(int)with no bounds checking:The
TemplateContext.LimitToStringproperty (default 1MB, set atTemplateContext.cs:147) does not prevent the allocation. This limit is only checked duringObjectToString()conversion (TemplateContext.Helpers.cs:101-103), which runs after the string has been fully allocated byPadLeft/PadRight. The dangerous allocation is the return value of a built-in function — it occurs before output rendering.The Scriban.AppService playground (
src/Scriban.AppService/Program.cs:63-140) exposesPOST /api/renderwith:CancellationTokenSource(line 118) — but this only cancels theawait Task.Run(...), not the runningtemplate.Render()call (line 122). The BCLPadLeftallocation completes atomically before the cancellation can take effect.PoC
Single request to crash or degrade the AppService:
This 39-byte template causes
PadLeft(500000000)to attempt allocating a 500-million character string (~1GB in .NET's UTF-16 encoding).Expected result: The service returns an error or truncated output safely.
Actual result: The .NET runtime attempts a ~1GB allocation. Depending on available memory, this either succeeds (consuming ~1GB until GC), or throws
OutOfMemoryExceptioncrashing the process.Sustained attack with rate limiting:
The
string.pad_rightvariant works identically:Impact
scriban-a7bhepbxcrbkctgf.canadacentral-01.azurewebsites.net.OutOfMemoryExceptionwith a single HTTP request.LimitToStringand timeout mitigations do not prevent the intermediate memory allocation.Recommended Fix
Add width validation in
StringFunctions.PadLeftandStringFunctions.PadRightto cap the maximum allocation. A reasonable upper bound is theLimitToStringvalue from theTemplateContext, or a fixed maximum if the context is not available:Alternatively, make the functions context-aware and use
LimitToStringas 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.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Scriban has Uncontrolled Recursion in
object.to_jsonCausing Unrecoverable Process Crash via StackOverflowExceptionGHSA-xcx6-vp38-8hr5
More information
Details
Summary
The
object.to_jsonbuiltin function in Scriban performs recursive JSON serialization via an internalWriteValue()static local function that has no depth limit, no circular reference detection, and no stack overflow guard. A Scriban template containing a self-referencing object passed toobject.to_jsontriggers unbounded recursion, causing aStackOverflowExceptionthat terminates the hosting .NET process. This is a fatal, unrecoverable crash —StackOverflowExceptioncannot be caught by user code in .NET.Details
The vulnerable code is the
WriteValue()static local function atsrc/Scriban/Functions/ObjectFunctions.cs:494:This function has none of the safety mechanisms present in other recursive paths:
ObjectToString()atTemplateContext.Helpers.cs:98checksObjectRecursionLimit(default 20)EnterRecursive()atTemplateContext.cs:957callsRuntimeHelpers.EnsureSufficientExecutionStack()CheckAbort()atTemplateContext.cs:464also callsEnsureSufficientExecutionStack()The
WriteValue()function bypasses all of these because it is a static local function that only takes theTemplateContextfor member access — it never callsEnterRecursive(), never checksObjectRecursionLimit, and never callsEnsureSufficientExecutionStack().Execution flow:
{{ x = {} }}x.self = x— stores a reference inScriptObject.Storedictionaryobject.to_json:x | object.to_json→ callsToJson()at line 477ToJson()callsWriteValue(context, writer, value)at line 488WriteValueenters theelsebranch (line 515), gets members via accessor, finds "self"TryGetValuereturnsxitself,WriteValuerecurses with the same object — infinite loopStackOverflowExceptionis thrown — fatal, cannot be caught, process terminatesPoC
In a hosting application:
Even without circular references, deeply nested objects can exhaust the stack since no depth limit is enforced:
Impact
StackOverflowExceptionterminates the .NET process.StackOverflowExceptioncannot be caught by application code. The hosting application cannot wraptemplate.Render()in a try/catch to survive this.object.to_jsonis a default builtin function (registered inBuiltinFunctions.cs), available in all Scriban templates unless explicitly removed.Recommended Fix
Add a depth counter parameter to
WriteValue()and check it againstObjectRecursionLimit, consistent with howObjectToStringis protected. Also addEnsureSufficientExecutionStack()as a safety net:Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Scriban: Denial of Service via Unbounded Cumulative Template Output Bypassing LimitToString
GHSA-m2p3-hwv5-xpqw
More information
Details
Summary
The
LimitToStringsafety limit (default 1MB since commitb5ac4bf) can be bypassed to allocate approximately 1GB of memory by exploiting the per-call reset of_currentToStringLengthinObjectToString. Each template expression rendered throughTemplateContext.Write(SourceSpan, object)triggers a separate top-levelObjectToStringcall that resets the length counter to zero, and the underlyingStringBuilderOutputhas no cumulative output size limit. An attacker who can supply a template can cause an out-of-memory condition in the host application.Details
The root cause is in
TemplateContext.Helpers.cs, in theObjectToStringmethod:Each time a template expression is rendered,
TemplateContext.Write(SourceSpan, object)callsObjectToString:The
StringBuilderOutput.Writemethod appends unconditionally with no size check:Execution flow:
LimitToStringdefault)forloop iterates up toLoopLimit(default 1000) timesWrite(span, x)→ObjectToString(x)ObjectToStringresets_currentToStringLength = 0since_objectToStringLevel == 0LimitToStringcheck (1,048,575 < 1,048,576)StringBuilder— no cumulative trackingPoC
Equivalent Scriban template:
Each of the 1000 loop iterations outputs a 1,048,575-character string. Each passes the per-call
LimitToStringcheck independently. Total output: ~1,000,000,000 characters (~1GB) allocated in theStringBuilder.Impact
LimitToStringlimit was specifically introduced to prevent resource exhaustion, but the per-call reset makes it ineffective against cumulative abuseRecommended Fix
Add a cumulative output size counter to
TemplateContextthat tracks total bytes written across allWritecalls, independent of the per-objectLimitToString:This provides defense-in-depth:
LimitToStringcaps individual object serialization, whileOutputLimitcaps total template output.Severity
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Scriban has Multiple Denial-of-Service Vectors via Unbounded Resource Consumption During Expression Evaluation
GHSA-xw6w-9jjh-p9cr
More information
Details
Summary
Scriban'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.Details
Vector 1: Unbounded string multiplication
In
ScriptBinaryExpression.cs, theCalculateToStringmethod handles thestring * intoperator by looping without any upper bound:The
LimitToStringsafety control (default 1MB) does not protect this code path. It only applies toObjectToStringoutput conversions inTemplateContext.Helpers.cs(lines 101-121), not to intermediate string values constructed insideCalculateToString. TheLoopLimitalso does not apply because this is a C#forloop, not a template-level loop —StepLoop()is never called here.Vector 2: Unbounded BigInteger shift left
The
CalculateLongWithIntandCalculateBigIntegerNoFitmethods handleShiftLeftwithout any bound on the shift amount:In contrast, the
Poweroperator at lines 722 and 795 usesBigInteger.ModPow(left, right, MaxBigInteger)to cap results. TheMaxBigIntegerconstant (BigInteger.One << 1024 * 1024, defined at line 690) already exists but is never applied to shift operations.Vector 3: LoopLimit bypass via range enumeration in builtin functions
The range operators
..and..<produce lazyIEnumerable<object>iterators:When these ranges are consumed by builtin functions,
LoopLimitis completely bypassed becauseStepLoop()is only called inScriptForStatementandScriptWhileStatement— it is never called in any function undersrc/Scriban/Functions/. For example:ArrayFunctions.Size(line 609) calls.Cast<object>().Count(), fully enumerating the rangeArrayFunctions.Join(line 388) iterates withforeachand appends to aStringBuilderwith no size limitPoC
Vector 1 — String multiplication OOM:
Vector 2 — BigInteger shift OOM:
Vector 3 — LoopLimit bypass via range + builtin: