Skip to main content

Free 30-min security demo Book Now

Offensive360 Offensive360
ZeroDays CVE-2026-74791
High CVE-2026-74791 CVSS 8.6 Scriban C#

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.

Offensive360 Research Team
Affects: < 7.0.0
Source Code

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:

  1. Request A (Tenant X, elevated privileges) renders a template that includes invoice_footer. Load() is called; authorization passes; the template content is stored in CachedTemplates["invoice_footer"].
  2. The context is returned to the pool.
  3. Request B (Tenant Y, lower privileges or different tenant) acquires the same context, calls Reset(), sets a new TemplateLoader bound to Tenant Y’s HTTP context, then renders a template that also includes invoice_footer.
  4. Scriban finds invoice_footer in CachedTemplates, skips Load() 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:

  1. 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.
  2. Cache-skipping authorizationITemplateLoader.Load() implementations that contain Unauthorized, permission checks, or tenant-scoping logic. The engine correlates these against call sites where CachedTemplates (or equivalent internal caches) are populated, checking whether cache hits bypass the security-bearing code path.
  3. 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

#authorization-bypass #template-engine #cache-poisoning #dotnet

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.