Scriban Stale Cache Authorization Bypass
CVE-2026-74791: Scriban's TemplateContext.Reset() fails to clear CachedTemplates, enabling cross-request template leakage in multi-tenant applications.
Overview
Scriban is a widely adopted .NET templating language and engine used across enterprise SaaS platforms, CMS products, and document generation pipelines. CVE-2026-74791 describes an authorization bypass rooted in Scriban’s TemplateContext lifecycle management: when TemplateContext.Reset() is called to prepare a context object for reuse, the internal CachedTemplates dictionary is not cleared. This means that any template previously loaded and cached during an earlier render pass — including templates that required authorization to load — remains resident in the context and can be re-rendered in a subsequent request without the application’s ITemplateLoader ever being invoked again.
The vulnerability was identified by security researchers analyzing Scriban’s context pooling behavior in high-throughput server environments where TemplateContext instances are reused across HTTP requests to reduce allocation pressure. In those patterns, which are common in production deployments, the incomplete reset creates a window where a lower-privileged user’s render can silently inherit and execute template content that was originally gated behind a permission check belonging to a prior user session.
Any application that (a) uses Scriban with a custom ITemplateLoader that enforces per-request authorization, (b) reuses TemplateContext instances via pooling or manual reset, and (c) processes template includes is potentially affected. Given how prevalent context reuse is in performance-conscious .NET services, the blast radius is broader than a casual reading of the advisory might suggest.
Technical Analysis
The root cause lives in the TemplateContext class. Scriban caches resolved templates in a Dictionary<string, Template> field named CachedTemplates. This dictionary is populated the first time a template is loaded via TemplateLoader.Load(), and subsequent include directives for the same path short-circuit directly to the cache, bypassing the loader entirely. This is correct and expected behavior for a single render lifetime.
The problem is that Reset() — the method intended to return a context to a clean baseline — does not call CachedTemplates.Clear(). The vulnerable pattern looks like this:
// Vulnerable: TemplateContext.Reset() before version 7.0.0
public virtual void Reset()
{
// Resets output, stack frames, variable scopes, etc.
Output.Clear();
_localVariables.Clear();
CurrentNode = null;
// ... other state cleared ...
// BUG: CachedTemplates is never cleared here.
// Any template loaded during the previous render
// remains in the dictionary.
}
Now consider a realistic pooled-context scenario in an ASP.NET Core application with a multi-tenant template loader:
// Vulnerable application code pattern
public class TenantAwareTemplateLoader : ITemplateLoader
{
private readonly IHttpContextAccessor _http;
private readonly ITemplateRepository _repo;
public TenantAwareTemplateLoader(
IHttpContextAccessor http,
ITemplateRepository repo)
{
_http = http;
_repo = repo;
}
public string GetPath(TemplateContext context, SourceSpan callerSpan, string templateName)
=> templateName;
public string Load(TemplateContext context, SourceSpan callerSpan, string templatePath, string templateContent)
{
var tenantId = _http.HttpContext.User.FindFirst("tenant_id")?.Value
?? throw new UnauthorizedAccessException("No tenant context.");
// Authorization check: ensure the requesting tenant owns this template
var template = _repo.GetTemplate(templatePath, tenantId)
?? throw new UnauthorizedAccessException($"Template '{templatePath}' not found for tenant {tenantId}.");
return template.Content;
}
}
// Somewhere in a render pipeline using a pooled context:
var context = _contextPool.Get(); // Returns a previously used context
context.Reset(); // Incomplete reset — cache survives
context.TemplateLoader = new TenantAwareTemplateLoader(_http, _repo);
var template = Template.Parse("{% include 'invoice_footer' %}");
template.Render(context); // 'invoice_footer' served from stale cache,
// Load() is never called, no authz check runs.
_contextPool.Return(context);
The sequence of events that enables exploitation:
- Request A (Tenant X, elevated privileges) renders a template that includes
invoice_footer.Load()is called; authorization passes; the template content is stored inCachedTemplates["invoice_footer"]. - The context is returned to the pool.
- Request B (Tenant Y, lower privileges or different tenant) acquires the same context, calls
Reset(), sets a newTemplateLoaderbound to Tenant Y’s HTTP context, then renders a template that also includesinvoice_footer. - Scriban finds
invoice_footerinCachedTemplates, skipsLoad()entirely, and renders Tenant X’s content into Tenant Y’s output — or vice versa.
The attacker does not need any special capability beyond the ability to trigger a render that references a template name that was previously resolved by a higher-privileged session. In a shared application server, this is largely a timing and pool-slot-acquisition problem, both of which are exploitable under moderate load.
Impact
The immediate consequence is unauthorized disclosure of template content across tenant or privilege boundaries. Depending on what templates contain — and in document generation, CMS, and invoicing systems they often embed sensitive business logic, PII-adjacent strings, pricing rules, or confidentiality notices — this constitutes a meaningful data breach vector.
The CVSS 8.6 HIGH score reflects a network-exploitable vulnerability with low attack complexity, no required privileges, and no user interaction (AV:N/AC:L/PR:N/UI:N). The scope is changed (S:C) because the impact crosses the security boundary established by the ITemplateLoader authorization model. Confidentiality impact is rated HIGH; integrity and availability are lower because the flaw is a read-side information leak rather than a write primitive.
In practice, enterprises running multi-tenant SaaS products on shared application server pools face the highest risk. Single-tenant applications that do not reuse TemplateContext instances are not meaningfully affected, though the latent bug still represents a maintenance hazard.
How to Fix It
The correct fix is to upgrade to Scriban 7.0.0 or later, which patches Reset() to explicitly clear CachedTemplates.
# .NET CLI
dotnet add package Scriban --version 7.0.0
# Package Manager Console
Install-Package Scriban -Version 7.0.0
# Direct edit of .csproj
<PackageReference Include="Scriban" Version="7.0.0" />
The corrected Reset() behavior in 7.0.0 includes the missing clear:
// Fixed: TemplateContext.Reset() in Scriban >= 7.0.0
public virtual void Reset()
{
Output.Clear();
_localVariables.Clear();
CurrentNode = null;
// ... other state ...
// FIX: stale cached templates are evicted on reset
CachedTemplates.Clear();
}
If an immediate upgrade is not possible, the interim workaround is to never reuse TemplateContext instances across request boundaries. Allocate a fresh context per render invocation:
// Safe: new context per request, no pool reuse
var context = new TemplateContext
{
TemplateLoader = new TenantAwareTemplateLoader(_http, _repo)
};
var template = Template.Parse("{% include 'invoice_footer' %}");
template.Render(context);
// context is discarded; GC handles cleanup
If pool-based allocation is required for performance reasons and upgrading immediately is blocked, extend TemplateContext with an override that clears the cache:
public class SafePooledTemplateContext : TemplateContext
{
public override void Reset()
{
base.Reset();
CachedTemplates.Clear(); // Backport the fix
}
}
Our Take
This vulnerability is a textbook example of a stateful object lifecycle bug — a class that accumulates sensitive state during its operational lifetime but provides an incomplete reset primitive. These bugs are particularly insidious in high-performance .NET code because the pooling patterns that expose them are also considered best practices for throughput. The developer who implements Reset() and the developer who implements the authorization-aware ITemplateLoader often aren’t the same person, and neither may fully appreciate the interaction.
From an enterprise security architecture standpoint, this reinforces why security-relevant operations like authorization checks should never be skippable via a cache without an explicit, auditable invalidation mechanism. When a cache hit bypasses a security check entirely — rather than caching the result of the check — you have a latent authorization bypass waiting for a lifecycle edge case to trigger it.
For enterprises running Scriban in document generation, e-commerce, or multi-tenant SaaS workloads, this warrants immediate triage. Check whether your application pools TemplateContext and whether your ITemplateLoader enforces per-request or per-tenant authorization.
Detection with SAST
This vulnerability class maps to CWE-668: Exposure of Resource to Wrong Sphere and secondarily to CWE-285: Improper Authorization. The triggering condition is a combination of object reuse across trust boundaries and a security control that is only applied during initial resource acquisition, not on cache retrieval.
Offensive360’s SAST engine flags this pattern by tracking:
- Object pool or manual reset patterns — any call to a
.Reset(),.Clear(), or.Initialize()method on a context-like object followed by assignment to a field that influences security decisions. - Cache-skipping authorization —
ITemplateLoader.Load()implementations that containUnauthorized, permission checks, or tenant-scoping logic. The engine correlates these against call sites whereCachedTemplates(or equivalent internal caches) are populated, checking whether cache hits bypass the security-bearing code path. - Incomplete state reset — dataflow analysis that models which mutable fields are cleared by a reset method and flags fields that carry security-sensitive data (template content, access-controlled resources) but are absent from the reset’s write set.
In dynamic testing, this class of bug can be surfaced by replaying authenticated requests across a shared connection pool with deliberate timing to induce context reuse, then asserting that tenant isolation is maintained in rendered output.
References
Detect this vulnerability class in your codebase
Offensive360 SAST scans your source code for CVE-2026-74791-class vulnerabilities and thousands of other patterns — across 60+ languages.