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
- Load Razor template bodies only from source code, deployment files or administrator-controlled storage.
- 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.
- Pass user data as model values. This does not replace encoding for the final HTML output context.
- Do not assume character removal, escaping or a helper named
sanitizemakes Razor source safe. If input templates must be accepted, use a clearly defined check such as an allow-list of complete fixed templates. - Do not trust legacy RazorEngine in-process Code Access Security (CAS) or
AppDomainisolation 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
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
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
CompileRenderStringAsyncis templatecontent. 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
Namevalue.