What is Server-Side Template Injection?
Server-Side Template Injection (SSTI) occurs when an application embeds user-controlled data directly into a template string that is subsequently rendered by a server-side template engine. Unlike reflected XSS, where injected content is interpreted by the browser, SSTI is evaluated on the server — often giving the attacker full Remote Code Execution (RCE) capability.
Template engines like Jinja2 (Python), Twig (PHP), Pebble (Java), and Razor (C#) provide powerful expression syntax for dynamic content. When user input is placed into the template itself (not just passed as a safe variable), an attacker can invoke built-in functions to read files, execute OS commands, or pivot further into the infrastructure.
How exploitation works
An application renders a greeting using a user-supplied name:
# VULNERABLE: Python / Jinja2
from jinja2 import Template
name = request.args.get('name')
template = Template("Hello, " + name + "!") # User input in template string
return template.render()
The attacker sends: name={{7*7}} and receives Hello, 49! — confirming template evaluation. From there, exploitation escalates:
# Read /etc/passwd
{{''.__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['popen']('cat /etc/passwd').read()}}
Vulnerable code examples
Python / Jinja2
# VULNERABLE: User input concatenated into template string
@app.route('/greet')
def greet():
name = request.args.get('name', 'World')
html = render_template_string("<h1>Hello, " + name + "</h1>")
return html
PHP / Twig
// VULNERABLE: Rendering user input as a template
$loader = new \Twig\Loader\ArrayLoader([]);
$twig = new \Twig\Environment($loader);
$template = $twig->createTemplate("Hello " . $_GET['name'] . "!");
echo $template->render([]);
Secure code examples
Python / Jinja2 — pass data as context variable
# SECURE: User input passed as a template variable, never embedded in template string
@app.route('/greet')
def greet():
name = request.args.get('name', 'World')
# Template is a static string; name is injected as a safe context variable
return render_template_string("<h1>Hello, {{ name }}</h1>", name=name)
Java / Thymeleaf — use model attributes
// SECURE: User data bound to model, never concatenated into template expression
@GetMapping("/greet")
public String greet(@RequestParam String name, Model model) {
model.addAttribute("name", name); // Thymeleaf auto-escapes th:text values
return "greet"; // References static templates/greet.html
}
<!-- greet.html — static template, data via th:text (auto-escaped) -->
<h1>Hello, <span th:text="${name}"></span>!</h1>
What Offensive360 detects
- User input in template strings — Calls to
render_template_string(),Template(),createTemplate(), or equivalent where the template argument includes tainted data - Dynamic template construction — String concatenation or interpolation used to build template expressions
- Unsafe Razor expressions —
@Html.Raw()or dynamic evaluation in Razor with user-supplied content - Handlebars/Mustache triple braces —
{{{userInput}}}that bypasses HTML escaping in JavaScript template engines
Remediation guidance
-
Never embed user input in template strings — Always pass user data as context/model variables. Template engines auto-escape variable values; they do not evaluate them as code.
-
Use static template files — Load templates from the filesystem (
render_template('greet.html', name=name)) rather than constructing template strings at runtime. -
Enable sandbox mode — Some engines (Jinja2, Twig) offer sandbox environments that restrict access to dangerous built-in objects. Enable and configure appropriately.
-
Apply output encoding — Ensure template output is correctly encoded for the rendering context (HTML, JavaScript, URL).
-
Audit third-party template rendering — Email generation, PDF rendering, and notification systems are common vectors for SSTI that are overlooked during code review.