Template injection

Untrusted input compiled or evaluated as C# template source

Description

Server-side template injection (SSTI) occurs when untrusted input is interpreted as executable template source rather than data. Razor templates can contain server-side C# directives and expressions. Someone who controls the template body can abuse the objects exposed to the engine and the application's process permissions.

Distinguish these values:

  • Template source: the body compiled or interpreted by the engine. Untrusted input in this position is dangerous.
  • Template key: an identifier for a registered template. Mapping a key to a fixed trusted template prevents users from supplying template code.
  • Model values: data displayed by the template. Pass untrusted values through the model instead of concatenating them into the body.

Potential impact

  • Disclosure of server secrets or application data
  • Access to files, networks or application functions
  • Denial of service through CPU or memory exhaustion
  • Arbitrary code execution, depending on the engine and execution environment

Remediation

  1. Load Razor template bodies only from source code, deployment files or administrator-controlled storage.
  2. Map user-selected identifiers to a fixed trusted template list. Do not rely on an untrusted collection or a list that can change outside the trusted configuration process.
  3. Pass user data as model values. This does not replace encoding for the final HTML output context.
  4. Do not assume character removal, escaping or a helper named sanitize makes Razor source safe. If input templates must be accepted, use a clearly defined check such as an allow-list of complete fixed templates.
  5. Do not trust legacy RazorEngine in-process Code Access Security (CAS) or AppDomain isolation as a security boundary. Most CAS APIs are obsolete in .NET 5 and later, and some calls may do nothing.

If users must author templates, consider a maintained constrained engine instead of one that executes C#, such as Razor. For Scriban, use at least 7.2.6, which fixes the cited execution-limit bypass. Create a fresh TemplateContext, expose only explicit ScriptObject/ScriptArray data and required functions, and set iteration, recursion, output, regex-time and cancellation limits. Do not expose rich .NET objects or a file-reading ITemplateLoader without restrictions. Scriban's safe runtime is not a process sandbox; use a separate low-privilege process or container when a strong boundary is required.

Examples

Before

csharp
using Microsoft.AspNetCore.Mvc;
using RazorLight;

async Task<string> Render(
    [FromBody] string template,
    RazorLightEngine engine)
{
    return await engine.CompileRenderStringAsync(
        "user-template",
        template, // Untrusted input is compiled and rendered as Razor source
        new { Name = "Ada" });
}

After

csharp
using Microsoft.AspNetCore.Mvc;
using RazorLight;

async Task<string> Render(
    [FromBody] string name,
    RazorLightEngine engine)
{
    const string trustedTemplate = "Hello @Model.Name";
    return await engine.CompileRenderStringAsync(
        "welcome",
        trustedTemplate,
        new { Name = name }); // Input is model data, not template source
}

Explanation:

  • Before: The second argument of CompileRenderStringAsync is template content. Passing a request body there lets the caller write Razor source.
  • After: The deployer controls the constant template body. Request data is passed only through the model's Name value.

References