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)
Affects ScribanSource GCVE Database
Public exploit available · CVSS 6.9 Medium : upgrade to 7.2.1 as a priority.
Data as of 7 Jul 2026, 12:28 UTC
- Patch
- Available – fixed in 7.2.1
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N/E:PWhat to do
Upgrade Scriban and Scriban.Signed to 7.2.1.
Affected software
| Product | Affected | Fixed in |
|---|---|---|
| NuGetScriban | Affected
| Fixed in
|
| NuGetScriban.Signed | Affected
| Fixed in
|
Check your own hosts for these packages with threat-finder (opens in a new tab), our free open-source scanner.
AI-suggested
If you cannot patch yet, avoid parsing untrusted or attacker-controlled templates or strings with vulnerable Scriban versions. Consider implementing external input validation or sandboxing to limit recursion depth before parsing.
Description
### Summary The `ExpressionDepthLimit` parser guard in Scriban does not actually stop parsing — it only logs a non-fatal error and lets recursive descent continue. As a result, a template containing a deeply nested expression (parentheses, array initializers, object initializers, or unary operators) drives the recursive-descent parser into a native stack overflow. The resulting `StackOverflowException` is **uncatchable** in .NET and **immediately terminates the host process**. Any application that parses an attacker-influenced template — or that passes attacker-controlled strings to `object.eval` / `object.eval_template` — can be crashed by a single small request (roughly an 8 KB payload). This is a denial-of-service. It affects both Scriban-native (`Template.Parse`) and Liquid (`Template.ParseLiquid`) syntax modes, which share the same expression parser. This re-opens two advisories that were reported as fixed: **GHSA-wgh7-7m3c-fx25** ("Uncontrolled recursion in parser → StackOverflow", reported fixed in 6.6.0) and **GHSA-p6q4-fgr8-vx4p** ("StackOverflow via nested array initializers bypasses ExpressionDepthLimit", reported fixed in 7.0.0). Both fixes are incomplete: the limit they rely on never halts recursion. **All releases 6.6.0 through 7.2.0 (current) are affected.** ### Details The depth guard is `EnterExpression()` in `src/Scriban/Parsing/Parser.Expressions.cs`: ```csharp // src/Scriban/Parsing/Parser.Expressions.cs:1209-1218 private void EnterExpression() { _expressionDepth++; var limit = Options.ExpressionDepthLimit; if (limit > 0 && !_isExpressionDepthLimitReached && _expressionDepth > limit) { LogError(GetSpanForToken(Previous), $"The statement depth limit `{limit}` was reached when parsing this statement"); _isExpressionDepthLimitReached = true; } } ``` When the limit is exceeded it calls `LogError(...)` and sets a flag. It does **not** throw, does **not** return a sentinel, and does **not** unwind the parse. `LogError` here uses the default `isFatal: false`, so it merely appends a message and sets `HasErrors` — parsing proceeds: ```csharp // src/Scriban/Parsing/Parser.cs:476-488 private void Log(LogMessage logMessage, bool isFatal = false) { Messages.Add(logMessage); if (logMessage.Type == ParserMessageType.Error) { HasErrors = true; if (isFatal) _hasFatalError = true; // not set on the depth-limit path } } ``` The flag `_isExpressionDepthLimitReached` is consulted **only** to avoid logging the same error more than once — no code path uses it to stop descending. Confirmed by full-repo search (`grep -rn "_isExpressionDepthLimitReached" src/`): it appears in exactly four places — the field declaration (`Parser.cs:40`), a reset to `false` (`Parser.cs:106`), and within `EnterExpression` the dedup test (`Parser.Expressions.cs:1213`) and its assignment to `true` (`:1216`). The only *read* is the dedup test on line 1213; nothing else reads it. `ParseExpression` calls `EnterExpression()` and then continues straight into the token switch with no flag check: ```csharp // src/Scriban/Parsing/Parser.Expressions.cs:113 + 181-182 EnterExpression(); try { ... case TokenType.OpenParen: leftOperand = ParseParenthesis(); // recurses back into ParseExpression ``` ```csharp // src/Scriban/Parsing/Parser.Expressions.cs:984-1001 private ScriptExpression ParseParenthesis() { var expression = Open<ScriptNestedExpression>(); ExpectAndParseTokenTo(expression.OpenParen, TokenType.OpenParen); expression.Expression = ExpectAndParseExpression(expression); // -> ParseExpression -> ParseParenthesis -> ... ... } ``` Both `Template.Parse` (Scriban-native) and `Template.ParseLiquid` (Liquid-compatibility) front-ends share this same expression parser, so both entry points are affected. So for input nested N levels deep, the parser recurses N levels deep regardless of `ExpressionDepthLimit`. There is no `RuntimeHelpers.EnsureSufficientExecutionStack()` call and no absolute recursion cap anywhere in the parser. Once the native thread stack is exhausted, the runtime raises `StackOverflowException`, which .NET does not allow to be caught and which tears down the entire process. The number of nesting levels required to overflow depends on the platform's thread-stack size (empirically around 4,000 levels on a default 1 MB stack); it is not a configurable mitigation. The same defective guard is what makes the array-initializer fix for GHSA-p6q4-fgr8-vx4p ineffective: `ParseArrayInitializer` was wrapped in `EnterExpression()/LeaveExpression()`, but because `EnterExpression()` only logs, the array path still overflows. The existing regression tests only assert `HasErrors == true` at a nesting depth of ~20 with a limit of 10 (`src/Scriban.Tests/TestParser.cs`); they never use a depth large enough to overflow the stack, so they pass while the protection does nothing against the actual DoS. **Runtime reachability without